Repository navigation
Restore the uv 0.2 pins hosted mode writes - #2014
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
Conversation
Hosted mode rewires uv 0.2 `[[distribution]]` locks (uv < 0.2.35) in all
three of their grammars, but the hosted unwind refused every such pin
("which grammar this lock follows is not derivable"), so `rollback`,
`remove` and `scan` could not undo what hosted wrote.
The grammar is derivable: it is the same evidence the rewrite follows.
The entry's own hosted source keeps its spelling (`"direct+<url>"` vs
`{ url }`), and the lock's other registry packages show the registry,
the artifact shape (`[distribution.sdist]` / `[[distribution.wheel]]`
sub-tables vs inline `sdist` / `wheels`) and the artifact keys. So the
unwind now restores these entries like `[[package]]` ones:
- the source goes back to `"registry+<R>"` or `{ registry = R }` in the
entry's spelling;
- artifacts are re-derived from PyPI in the siblings' shape, with uv
<= 0.2.5's `wheel` key and its sub-table order (sdist before the
entry's `[[distribution.dependencies]]`, wheels after);
- dependents' references, inline or `[[distribution.dependencies]]`
tables with string sources, are pointed back.
The paired pyproject is restored as before. Vendored still refuses these
locks (no portable local wheel spelling), unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
|
Labeled Ready for review (burn-down agent).
Generated by Claude Code |
Review fixes for the uv 0.2 `[[distribution]]` hosted unwind, checked against real `uv lock` output from uv 0.1.45 to 0.2.35: - Every one of those releases records every file of a release on every platform, so a pure-Python 3 release re-derives faithfully in all three grammars. The tests now round-trip real 0.1.45, 0.2.5, 0.2.15, 0.2.16, 0.2.18 and 0.2.34 locks (tests/fixtures/upstream/uv-0.2). - uv <= 0.2.5 write an entry's sdist and wheel sub-tables before its dependency tables; the unwind had the wheel after them. - uv 0.2.18-0.2.22 write an inline sdist before inline dependencies, 0.2.23-0.2.34 after them, and nothing else in the lock tells them apart. The order is read off another registry entry carrying both; without one the rewrite refuses to wire the entry and the unwind refuses to restore it. - uv 0.2.12-0.2.27 apply an exclude-newer cutoff from [tool.uv], a project or user uv.toml, or UV_EXCLUDE_NEWER without recording it; 0.2.28-0.2.33 record it at the lock's top level. The unwind refuses on any of them for the inline grammar (and on the top-level key), and the rewrite refuses a lock whose project sets one. - One grammar detector in utils/python_lock.rs serves both directions: the entry's own artifacts decide, then any other entry's (the rewrite's whole-text search is gone). - Sources are restored through UvSource in their own spelling, a standard [distribution.source] table included, with accurate errors; artifact_tables reuses table_likes. Docs: CLI_CONTRACT.md and docs/testing/uv-compatibility.md describe the restore, the refusals and the measured boundaries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
sets_exclude_newer matched every key starting with exclude-newer, so a project setting exclude-newer-package (a per-package cutoff, or a false exemption) made hosted rewrite and the uv 0.2 unwind refuse every inline [[distribution]] pin. uv 0.2.12-0.2.27, the releases whose unrecorded cutoff this guards against, predate that key and ignore it, so only the global exclude-newer cutoff blocks those locks. The pyproject [options] path in upstream/uv.rs already excluded the key the same way. Found by Cursor Bugbot on ab4f0f8. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011f9aeQKNTJYztmYaJaAEsc
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 80180b8. Configure here.
|
Labeled Ready for review again (burn-down agent).
Generated by Claude Code |
Final review brief (for the human approver)What it does. Hosted mode already rewires uv 0.2 Risk: medium. It changes how lockfiles are written on the unwind path, but every gap it can't prove fails closed with a precise refusal. Round trips are checked byte for byte against real Look here
Verified
Changes I made: none. Open questions (non-blocking)
Auto-merge is not armed yet because Generated by Claude Code |
Second review round, plus the known follow-up. - A uv lock that records an `exclude-newer` cutoff for the package (a global one under `[options]` or, for uv 0.2.28-0.2.33, at the top level, or an `exclude-newer-package` naming it) was still wired by hosted mode, though the unwind always refuses such a pin. The rewrite now refuses it (`redirect_uv_lock_unsupported`), for `[[package]]` and `[[distribution]]` locks alike. One classifier, `upstream::lock_cutoff`, serves the rewrite and the unwind. - The rewrite's project-settings cutoff check for `[[distribution]]` locks re-implemented uv settings discovery and missed the settings file above the root (`UV_ANCESTOR_CONFIG_KEY`) the unwind reads. It now uses `pylock_uv_settings` / `root_uv_settings`, computed once per run rather than per dep. - The sdist / inline-dependencies refusal ran before the not-applicable checks, turning an entry the rewrite does not touch (a non-registry, non-direct source) into a hard refusal. It now runs after them. - docs/testing/uv-compatibility.md no longer contradicts itself on which release files a 0.2.x lock records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Hosted mode rewires uv 0.2
[[distribution]]locks (uv 0.1.45–0.2.34) in all three of their grammars (utils/python_lock.rsallowsArtifactSource::Urlthere). The hosted unwind (patch/redirect/upstream/uv.rs) refused every such pin: "uv 0.2[[distribution]]locks changed their artifact grammar across releases; which one this lock follows is not derivable". Sorollback,removeandscancould not undo what hosted had written.Decision: option (a), restore faithfully, and refuse where that can't be proven
The grammar is derivable from the same evidence the hosted rewrite uses to choose it: the entry itself. The rewrite keeps the entry's source spelling (
"direct+<url>"for uv ≤ 0.2.17,{ url }after) and writes its artifacts in the entry's own grammar ([[distribution.wheel]]sub-tables for uv ≤ 0.2.5, inlinewheelsfrom 0.2.6). The unwind reads both back off the hosted entry, and gets the artifact keys and registry from the sibling registry packages.I checked faithfulness against real uv binaries (uv 0.1.45 through 0.2.35, macOS arm64, CPython 3.11, locking
urllib3,markupsafe,python-dateutilandisodateagainst live PyPI):markupsafe 2.1.5wheels were recorded through 0.2.18, and 40 from a release between 0.2.19 and 0.2.25, which dropped the wheelsrequires-pythonexcludes. The unwind only restores pure-Python 3 releases, andrequires-pythoncan't drop those wheels, so re-deriving every PyPI file is faithful for all three grammars. Supported range: 0.1.45–0.2.34.[distribution.sdist],[[distribution.wheel]], then[[distribution.dependencies]]. My first commit put the wheel after the dependencies. Its synthetic test passed only because it encoded the same wrong assumption.sdistbefore inlinedependencies, and 0.2.23–0.2.34 after them. Nothing else in the lock tells these releases apart (the locks are otherwise byte-identical), and a hosted entry has lost itssdist. The only evidence is another registry entry that carries both.exclude-newer(uv lock):[tool.uv]and the env var, and errors onexclude-newerinuv.toml).--exclude-newer.UV_EXCLUDE_NEWER,[tool.uv] exclude-newerand project or useruv.toml.[options].So the unwind restores, and it refuses with a precise reason when the lock lacks something it needs.
Change
utils/python_lock.rs: shared detectors used by both the rewrite and the unwind:distribution_artifact_tables: the entry's own artifacts decide its grammar, then any other entry's. This replaces the rewrite's whole-text search.distribution_sdist_before_dependencies: reads the order off registry entries only.sets_exclude_newer.UvSource::of_item/of_value.textargument is gone fromplan_python_lock_rewrite/PythonLockSession::plan.patch/redirect/mod.rs): refuses (redirect_uv_lock_unsupported) two cases it could not undo:[[distribution]]entry with an sdist and inlinedependencieswhen no other registry entry shows their order;[[distribution]]lock whose project (uv.toml, else[tool.uv]) setsexclude-newer.patch/redirect/upstream/uv.rs):UvSource, including a standard[distribution.source]table.[[distribution.wheel]]key), or thesdistplaced beside inlinedependenciesas a sibling shows. When no sibling shows that order, it refuses.[[distribution.dependencies]]tables.uv.toml,[tool.uv], orUV_EXCLUDE_NEWER) for the inline grammar, and a top-levelexclude-newer(0.2.28–0.2.33) like an[options]one.artifact_tablesnow reusestable_likes.Vendored still refuses
[[distribution]]locks (no portable local wheel), unchanged. Discovery and the lock inventory already read all three grammars.Review findings (
/code-review high)exclude-newer(confirmed; fixed by refusing). uv 0.2.12–0.2.27 apply a cutoff from[tool.uv], a project or useruv.toml, orUV_EXCLUDE_NEWERwithout recording it.[options](the old check read only[options]).[options]refusal (Hosted uv rollback / remove ignoreexclude-newer-package: on uv 0.12+ the restored uv.lock gains an sdist the per-package cutoff had excluded, so the restore isn't the original lock #1604). Date-only cutoffs are in local time, so filtering wouldn't be exact either.uv lock --exclude-newerto 0.2.6–0.2.27 leaves no trace in the lock, the project or the environment. A wrong restore then needs the cutoff to land between the upload times of one release's own files.sdist/dependenciesflip. Both are fixed.uv lockoutput from 0.1.45, 0.2.5, 0.2.15, 0.2.16, 0.2.18 and 0.2.34 (tests/fixtures/upstream/uv-0.2/).distribution_artifact_tablesis used by both directions; the entry's own artifacts come first. New testuv_distribution_entry_grammar_wins_over_the_first_sibling(first sibling inline, hit in sub-tables), plus a unit test of both detectors.CLI_CONTRACT.mdnow says so. The test above uses a hit with a string source and a sibling with an inline-table source.artifact_tablesduplicatedtable_likes(confirmed; fixed).restored_sourcehand-parsed the source (confirmed; fixed). It now goes throughUvSource::of_value/of_itemandUvSource::url, the same parserlock_hitsuses.[distribution.source]table unhandled (confirmed; fixed). A standard source table is restored in that spelling (testuv_distribution_source_table_is_restored). A missing source and a source that no longer names the patch each get their own message.[[distribution]]row and the limits section now cover the unwind, the tested versions and the measured boundaries.Tests
tests/upstream_restore_golden.rs. Each one fails on the previous commit (234b65e) and passes now:uv_distribution_locks_round_trip: six real uv locks, pinsurllib3andurllib3+python-dateutil; paired pyproject in LF and CRLF, plus lock-only. All byte-exact.uv_distribution_sdist_order_without_evidence_is_refused: covers both the rewrite and the unwind.uv_distribution_entry_grammar_wins_over_the_first_siblinguv_distribution_exclude_newer_is_refused: covers pyproject,uv.toml, a top-level lock key andUV_EXCLUDE_NEWER; 0.2.5 still round-trips.uv_distribution_source_table_is_restoredutils::python_lock::tests::distribution_grammar_reads_the_entry_before_its_siblings.cargo test -p socket-patch-core --lib -- patch::redirect:: utils::python_lock vex::discover:: vendor::pypi: 1857 passed. That includes thepython_lock_equivalencegoldens, which are unchanged.committed_fixture_corpus_matches_goldenpasses. Golden change:vex-discover-golden/upstream.jsongains entries for the six new pristine fixtures (no refs; registryelsewhereevidence only).--test upstream_restore_golden: 58 passed.--test uv_hosted: 4 passed. CLIin_process_rollback_hosted(uv override and pypi filters): 6 passed.cargo clippy --locked --workspace --all-features -- -D warningsandcargo fmt --all -- --checkare clean.Not done
Hosted still wires aDone in round 2 ([[package]]lock whose[options]records a cutoff.be7384f).uv lockbytes instead.Review findings, round 2 (
/code-review highonab4f0f8+80180b8, cloud routine), and theexclude-newerfollow-upFixed in
be7384f. Each test below failed on80180b8.redirect_uv_lock_unsupported): a uv lock with[options] exclude-newer, a top-levelexclude-newer(uv 0.2.28–0.2.33), or anexclude-newer-packagenaming the package (unless it exempts it withfalse). This covers[[package]]and[[distribution]]locks alike. One classifier,upstream::lock_cutoff, is now shared by the rewrite and the unwind, replacing the unwind's inline copy ofsets_exclude_newer. Tests:patch::redirect::tests::uv_lock_recording_a_cutoff_is_not_wired, anduv_distribution_exclude_newer_is_refused("recorded" case).uv_refusalsanduv_exclude_newer_package_refuses_only_its_packagenow wire before the cutoff is recorded, and also assert that hosted refuses a lock that already records it.distribution_project_cutoffre-implemented uv settings discovery. It now readspylock_uv_settings(that is,root_uv_settings, includingUV_ANCESTOR_CONFIG_KEY), the same files the unwind reads. An invaliduv.tomlstops the lookup as it does in uv. The check is computed once per run, not once per dep. Test:uv_distribution_exclude_newer_is_refused("ancestor" case).redirect_uv_entry_not_foundagain, not a hard refusal. Test:utils::python_lock::tests::a_not_applicable_distribution_entry_is_not_refused_for_its_sdist.docs/testing/uv-compatibility.mdnow matches the module docs: 0.2.x locks record each file thatrequires-pythonadmits.Declined:
uv.tomlandUV_EXCLUDE_NEWER. The rewrite reads only project files, and the scan can't see the machine that will lock or roll back. The unwind still refuses on them, which fails closed, and the refusal names the setting.wheel_targetsprobe fetches metadata for deps the rewrite then refuses. That costs only network time and doesn't affect correctness.restore_uv_entry's duplicatedtablesflag. A refactor only; both values are set together today.Tests (cloud run):
cargo test -p socket-patch-core --lib -- patch::redirect:: utils::python vex::discover:: vendor::pypi hosted::gives 2061 passed.--test upstream_restore_goldengives 58 passed. CLImode_migration_pypigives 79 passed, andin_process_redirectgives 149 passed. clippy and fmt are clean. The 3 lib failures and 3in_process_redirectfailures are write-failure tests that chmod a directory read-only, which root (this container) ignores. They're unrelated to this PR.Note
Medium Risk
Changes hosted rollback/remove for legacy uv locks and PyPI hash re-derivation, with explicit refusals when metadata is ambiguous; scope is lockfile editing rather than auth or runtime install paths.
Overview
Hosted rollback/remove no longer blanket-refuses uv 0.2
[[distribution]]locks (0.1.45–0.2.34). The upstream restore path rewrites pins back to PyPI in the same grammar the hosted entry used—string vs inlinesource, sub-table vs inline artifacts,sdistplacement relative to inlinedependencies, and dependent references including[[distribution.dependencies]].Shared logic in
python_lock.rs(distribution_artifact_tables,distribution_sdist_before_dependencies,sets_exclude_newer) drives both the hosted rewrite and unwind so shape detection stays aligned.PythonLockSession::planno longer takes lock text; planning uses the parsed document only.New refusals (rewrite and/or unwind) when rollback cannot be proven faithful: entries with both
sdistand inlinedependenciesbut no sibling registry entry showing uv’s 0.2.18–0.2.34 ordering; inline-artifact locks when project/userexclude-newerorUV_EXCLUDE_NEWERapplies without being recorded (plus top-level lock cutoffs for 0.2.28–0.2.33). Vendored[[distribution]]behavior is unchanged.Docs (
CLI_CONTRACT.md,uv-compatibility.md) and golden tests cover the new restore rules; six realuv lockfixtures round-trip through hosted rewrite and unwind.Reviewed by Cursor Bugbot for commit 80180b8. Configure here.
Generated by Claude Code