Skip to content

uv: update lockstep-pinned dependencies together - #16509

Draft
wenceslas-sanchez wants to merge 26 commits into
dependabot:mainfrom
wenceslas-sanchez:uv-lockstep-full-unlock
Draft

wenceslas-sanchez wants to merge 26 commits into
dependabot:mainfrom
wenceslas-sanchez:uv-lockstep-full-unlock

Conversation

@wenceslas-sanchez

@wenceslas-sanchez wenceslas-sanchez commented Oct 8, 2026 •

Copy link
Copy Markdown

What are you trying to accomplish?

Fixes #16390 (second half, stacked on #16507).

Stacked on #16507: the first 4 commits here are that PR, only the last 22 are new. Clean diff on top of it: wenceslas-sanchez#1

When two direct dependencies are pinned in lockstep (opentelemetry-api==1.25.0 + opentelemetry-sdk==1.25.0,
or the httpx2/httpcore2 case in the issue), neither can move alone and Dependabot skips both, grouped or not.

This implements full unlock for uv, behind the uv_lockstep_full_unlock experiment:

  • For a pyproject dependency that has another top-level dependency next to it in uv.lock, :own now asks uv
    instead of trusting the registry. If uv's conflict names another direct dependency, the ladder moves on to
    :all.
  • :all works like Bundler's force updater: rewrite the dependency's pin, run uv lock, and on a conflict take
    the package names from uv's full conflict output (including the text behind UpdateNotPossible, not just the
    short message). Those peers are relaxed to >= <locked> and the lock is retried, at most 3 rounds. Peers don't
    get --upgrade-package, so uv only moves a peer when it has to, instead of to its latest release.
  • It returns the dependency first, then the peers that moved, sorted by name.

Anything you want to highlight for special attention from reviewers?

  • Peer discovery reads uv's error text, like npm does with peer-dependency warnings. Only a conflict that names
    another direct dependency counts. Any other resolution failure (a new release needing a newer Python, or uv
    rewording its message so we find no names) keeps today's behaviour: the file updater reports it as before.
    Failures that aren't resolution failures (build, auth, network) now come out of the :own check instead, with
    the same error.
  • Like npm and Bundler, ignore conditions aren't applied to peers. Cooldown is. Group update-types rules aren't
    applied to peers either, so a peer can take a major bump inside a patch/minor-only group.
  • Like npm, a peer outside the group still gets bumped inside the grouped PR, but isn't listed in its title.
    Grouping lockstep packages together avoids that.
  • In ungrouped jobs, if uv moves a peer to a version below its latest, the peer's own turn can still open a
    second PR that overlaps with this one.
  • Cost: one extra uv lock for pyproject dependencies that have a top-level neighbour in uv.lock (dependencies
    only in uv.lock already go through uv, so they aren't probed again). :all reuses that run
    as its first round, so a real conflict adds one more uv lock per discovery round (usually one). That's the main
    reason it's behind an experiment.
  • Calling :all directly on a fresh checker, before :own has been asked, just returns false.
  • pip/poetry still have the full unlock stub; happy to look at them separately if this approach is OK.

How will you know you've accomplished your goal?

Specs for the resolver (forward and reverse direction, a peer found further down uv's derivation, multi-round,
giving up when a round finds no new peer, a peer that uv forks onto several versions before or after the lock,
extras, cooldown in and out of the window, conflicts that name no other direct dependency, the peer filters that
skip build-system-only, local and unversioned dependencies, and reuse of the :own run) and for the checker
(experiment on/off, :own vs :all, the security fix stopping at the lowest fixed version). The checker specs
that matter most run the real resolver and LockFileUpdater with only the uv shell command, the Python install
and PyPI stubbed, feeding them real conflict messages from uv 0.11.25 and from 0.12.20 (same output as the 0.12.18 dependabot ships): they check that only
--upgrade-package opentelemetry-api==1.26.0 is passed, that sdk is relaxed to >=1.25.0, that :own then
:all takes two uv lock runs, and that asking about the peer's side (sdk) also reports it can't move alone.

The name parsing is also checked against nine conflict messages excerpted from uv's own test snapshots
(forked {marker} nodes, extras, dependency groups, Python-version conflicts, ranges) plus two generated with uv
(0.11.25 and 0.12.20). Names follow PEP 508 the way uv-pep508 parses them.

One thing I hit along the way: since uv 0.12.14 (picked up in #16276) uv prints conflicts as error: No solution found … /
cause: … instead of × No solution found …, so LockFileErrorHandler::UV_UNRESOLVABLE_REGEX stopped matching. This PR
only makes the lockstep resolver recognise both formats; the handler fix for everyone else is #16511 (issue #16510).

I also ran the checker end to end with no stubs (real uv 0.12.18 from the dev image, real PyPI, experiment on)
on the lockstep fixture: :own is false, :all is true, and api and sdk both go 1.25.0 → 1.45.1 in pyproject.toml
and uv.lock.

Locally: bin/test uv (1303 examples, 0 failures, 2 pending that are skipped by the spec helper) and RuboCop
(no offenses). srb tc doesn't work in my image, so I ran the sorbet binary directly with the repo's
sorbet/config: no errors.

Checklist

  • I have run the complete test suite to ensure all tests and linters pass.
  • I have thoroughly tested my code changes to ensure they work as expected, including adding additional tests for new functionality.
  • I have written clear and descriptive commit messages.
  • I have provided a detailed description of the changes in the pull request, including the problem it addresses, how it fixes the problem, and any relevant details about the implementation.
  • I have ensured that the code is well-documented and easy to understand.

Comment thread uv/lib/dependabot/uv/file_updater/lock_file_error_handler.rb Fixed
@wenceslas-sanchez

Copy link
Copy Markdown
Author

Heads-up: the uv ≥ 0.12.14 error format mentioned above is now tracked in #16510, with a fix in #16511. The note in the description dates it to #15770, but it actually started with #16276 (0.12.7 → 0.12.15). I'll update the description once #16511 settles.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

uv: resolve dependency group members together so lockstep-pinned packages can be updated

2 participants