Add experimental uv resolver for Python lockfile generation#23212
Add experimental uv resolver for Python lockfile generation#23212Affanmir wants to merge 5 commits into
Conversation
Add `[python].lockfile_resolver = "uv"` option that uses `uv pip compile` to pre-resolve pinned requirements before running `pex lock create --no-transitive`. This provides 3-4x faster lockfile generation on cold cache by leveraging uv's PubGrub resolver. All lock styles are supported including `universal` (via `uv pip compile --universal`). Closes pantsbuild#20679 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
Hi @Affanmir, thanks for this contribution. I have some questions about the background, motivation, and creation of this change. It is unusual to receive a PR that directly competes with an existing, pending PR by another contributor that is already going through code review. We like to keep things collegial and friendly here, and so in this case it might have been appropriate to first discuss your change with the author of that PR, and with the maintainers of the project. It would be helpful if you could jump on Slack, introduce yourself there, and give us a bit of background about your use of Pants, your motivation for this change, and how you created it using Claude. Thanks! |
|
Hi! As far as I can see, this PR solves all the issues related to UV for Pants. Can you tell me if there's anything I can do to help make sure these changes make it to the main branch? |
We are still reviewing this change, and considering whether this is the way to go WRT uv integration. We intend to get either something along the lines of this PR, or something that uses a different approach, merged soon. |
We are going to discuss this at the monthly Pants developer meeting on Monday, which is a huddle in the #development channel on Slack, this Monday (4/13) at 1 PM PST, and all are welcome to join and participate! |
|
will take a look this week in conjunction with our internal needs. |
|
Hi @Affanmir , thanks for the contribution! The implementation looks reasonable to me. Although for our specific needs, So we would ask for uv to handle a matrix of, for example, I don't think uv handles that natively, so this will become a 2x2=4 uv calls, and then we need to synthesize the result. @benjyw how do you feel about it? I am fine landing this one first, and then we can add our ask later. It should be complimentary/additive to this implementation. |
I'm not sure how we would synthesize the result (which is a fully-pinned requirements.txt). And for If we were using |
Ah I see. Thanks! uv's output is just a requirements.txt, so perf impact should be small (if not constraining python versions and platforms) The potential bigger perf impact is around pex turning the requirements.txt into the pex lock, iiuc it is going to download all applicable linux and mac wheels, which is more than Ref: |
|
So point is that I think we can land this, but may need to follow up on adding a bit more platform constraints to help with perf. |
That may be, but also the LLM BS is BS. Back in October the 2.64.0 release had Pex defaulting to 0 downloads for modern Pips and indexes like PyPI that serve PEP-658 distribution metadata . It would be really awesome if Pants folks stopped arm-chairing and actually got their hands dirty and did real work to suss all this out. @wisechengyi FWIW, in your dialogue with the LLM, the LLM gets several other facts wrong. This is a super poor basis for decision making. Even without LLMs developers tend to be pro hand-wavers. You, traditionally, have not been one of these; so I find this worrying! |
|
Was following this PR since it's related to #23197 and ran some benchmarks. Turns out the bottleneck is pex's artifact enumeration, not the resolve step.
Cold cache (3 runs, cache cleared before each):
Warm cache (3 runs, warmup 1):
Both uv approaches resolve the same 137 packages at identical versions. Output is Reproduction: hyperfine --runs 3 --warmup 1 \
-n "pex3 lock create" \
"pex3 lock create --style=universal --target-system linux --target-system mac \
--interpreter-constraint 'CPython==3.11.*' -r requirements.txt -o out.json" \
-n "this PR: uv+pex" \
"uv pip compile requirements.txt --python-version 3.11 --no-header --universal -o pinned.txt \
&& pex3 lock create --style=universal --target-system linux --target-system mac \
--interpreter-constraint 'CPython==3.11.*' --no-transitive -r pinned.txt -o out.json" \
-n "bypass pex: uv pip compile" \
"uv pip compile requirements.txt --python-version 3.11 \
--generate-hashes --no-header --universal -o out.txt"The real speedup comes from skipping |
|
Yes. Thanks for the perf breakdown. Unfortunately, that was a bit of a waste of your time. This has been known for a good while now, but Pants folks have been super poor about getting a hold of the tangle of issues, PRs and slack threads. I incessantly hound them, which they don't respond well (or at all) to. I'm just pointing out yet again that your time is being wasted by Pants maintainers not gathering threads, making a plan and announcing it. @seungwoo-ji-03, for example, are you aware your merged PR may have been wasted effort itself?: https://pantsbuild.slack.com/archives/C0D7TNJHL/p1777401294195339?thread_ts=1777401294.195339&cid=C0D7TNJHL |
|
@jsirois Thanks for pointing this out — I wasn't aware of benjyw's pending work. |
|
Yes, sorry, @seungwoo-ji-03, I should have been much more public about this work (which I started after #23197 was merged, after discussion at the recent developer monthly call). Just got a lot going on, and trying to get this work done in the time cracks between everything else. That said, I have a mostly-working implementation of uv lockfile support (inspired by this PR and by your #23197) that doesn't require the intermediate conversions and pex invocations, and creates pexes as needed from a uv-synced venv. I'm ironing out an issue with console scripts, and then should have it ready for review, hopefully today. |
|
Note that this is superseded by the more comprehensive solution in #23302. Thanks for all the effort on this front, and for the prodding to finally get this done properly. |
uv lockfiles are first-class entities that users check in. When a pex is needed, we use uv to create a venv, which it does very efficiently, and then use pex's `--venv-repository` feature to build a pex from (a subset of) that venv's requirements. Note that we still create VenvPexes to run tools, and in some cases this might be unnecessary, since we already have the uv venv. But utilizing those venvs directly would be an even bigger change (and this one is already massive), and there are caching tradeoffs to figure out . So we leave that for the future (and probably for the new python backend). Note also that the dynamics of lockfile generation remain pants's (you must generate explicitly) and not uv's (lockfiles are automatically updated for you when input requirements change). The latter is desirable, but again this is for the future. The general thrust of the changes is: - Add uv as a third lockfile format (after pex and the deprecated legacy constraints file "locking") - Update the lockfile metadata to include the format (and also the name of the resolve that created the lockfile). - Make lockfile metadata reading much more robust and useful, so we can get the format from it reliably. Unfortunately this logic is tricky since it has to handle pex lockfiles both with and without separate metadata files. For uv we simply require separate metadata files and do not add any metadata inside the lockfile itself, as we foolishly did for pex lockfiles at one point. - Update the lockfile diffing code to support diffing across all combinations of old/new and pex/uv. - Update the existing `uv` subsystem for more general use. - Add src/python/pants/backend/python/util_rules/uv.py, which runs `uv`, first generating ephemeral pyproject.toml and uv.toml config files. We handle environments correctly, which requires some `realpath` shenanigans. - Update some iffy logic in PythonToolBase that parses lockfiles to find useful info for help messages. It was previously relying on pex lockfile internals (which are not guaranteed to be stable) and it now also relies on uv lockfile internals (similarly not stable). However now at least it fails gracefully if the lockfile parsing fails and just prints a less useful error message. - Use all this in src/python/pants/backend/python/util_rules/pex.py, instead of the previous, uv pex building solution. Note that this supersedes the more limited (but much appreciated) change in #23197 that implemented only the uv venv -> pex part. It also supersedes the competing pending changes in #22949 and #23212, which implemented partial support for uv resolution.
|
Closing since #23302 is now merged. We'd love your feedback on that one (I do have your first round of fixes, and will look at how to apply them). Thanks! |
…ick of #23302) (#23320) uv lockfiles are first-class entities that users check in. When a pex is needed, we use uv to create a venv, which it does very efficiently, and then use pex's `--venv-repository` feature to build a pex from (a subset of) that venv's requirements. Note that we still create VenvPexes to run tools, and in some cases this might be unnecessary, since we already have the uv venv. But utilizing those venvs directly would be an even bigger change (and this one is already massive), and there are caching tradeoffs to figure out . So we leave that for the future (and probably for the new python backend). Note also that the dynamics of lockfile generation remain pants's (you must generate explicitly) and not uv's (lockfiles are automatically updated for you when input requirements change). The latter is desirable, but again this is for the future. The general thrust of the changes is: - Add uv as a third lockfile format (after pex and the deprecated legacy constraints file "locking") - Update the lockfile metadata to include the format (and also the name of the resolve that created the lockfile). - Make lockfile metadata reading much more robust and useful, so we can get the format from it reliably. Unfortunately this logic is tricky since it has to handle pex lockfiles both with and without separate metadata files. For uv we simply require separate metadata files and do not add any metadata inside the lockfile itself, as we foolishly did for pex lockfiles at one point. - Update the lockfile diffing code to support diffing across all combinations of old/new and pex/uv. - Update the existing `uv` subsystem for more general use. - Add src/python/pants/backend/python/util_rules/uv.py, which runs `uv`, first generating ephemeral pyproject.toml and uv.toml config files. We handle environments correctly, which requires some `realpath` shenanigans. - Update some iffy logic in PythonToolBase that parses lockfiles to find useful info for help messages. It was previously relying on pex lockfile internals (which are not guaranteed to be stable) and it now also relies on uv lockfile internals (similarly not stable). However now at least it fails gracefully if the lockfile parsing fails and just prints a less useful error message. - Use all this in src/python/pants/backend/python/util_rules/pex.py, instead of the previous, uv pex building solution. Note that this supersedes the more limited (but much appreciated) change in #23197 that implemented only the uv venv -> pex part. It also supersedes the competing pending changes in #22949 and #23212, which implemented partial support for uv resolution.
Summary
[python].lockfile_resolver = "uv"option (default:pex) for faster lockfile generationuv pip compileto pre-resolve pinned requirements, thenpex lock create --no-transitiveto materialize the PEX lockfileuniversal(viauv pip compile --universal)[uv].args_for_lockfile_resolvefor passthrough flags (e.g.,--index-strategy unsafe-first-match)Motivation
Issue: #20679 — faster, more ergonomic lockfile generation by leveraging uv's PubGrub resolver.
This builds on the uv PEX builder from #23197 (merged) and is inspired by the approach in #22949, with key improvements:
--universal[uv].args(general)[uv].args_for_lockfile_resolve(specific)Benchmarks
Environment: Apple M4, macOS 15.x (Darwin 24.6.0), arm64
Dataset: 87 top-level requirements (Trio test deps + web/data/cloud ecosystem) → 169 resolved packages
Tool: hyperfine v1.20.0
Methodology: Same dataset structure as uv's own benchmarks (Trio project requirements)
Resolver-only comparison (no Pants overhead)
Isolates the dependency resolution step —
uv pip compilevspex lock create:pex lock create(pip)uv pip compileEnd-to-end Pants comparison
Full
pants generate-lockfiles --resolve=...(includes engine init, interpreter discovery, PEX CLI bootstrap):lockfile_resolver = "pex"lockfile_resolver = "uv"Note: End-to-end improvement is smaller because Pants engine overhead (~20s) is constant. The uv resolver also shows much lower variance (±0.8s vs ±13.7s), providing more consistent lockfile generation times.
Correctness
Both resolvers produce equivalent lockfiles — 168/169 packages identical, with minor differences in optional/conditional dependency selection (
greenletvstomli), which is expected behavior between different resolvers.Reproduction
Disclaimers
--no-pantsdused for all measurements (no daemon memoization)How it works
Usage
Current limitations
complete_platformsoverrides/sources/excludesCode changes
subsystems/setup.pyLockfileResolverenum +lockfile_resolveroptionsubsystems/uv.pyargs_for_lockfile_resolvepassthrough + updatedDownloadedUvgoals/lockfile.pygenerate_lockfile()goals/lockfile_test.pylockfiles.mdx2.32.x.mdTest plan
test_strip_named_repo— unit test for index URL stripping helpertest_uv_resolver_rejects_complete_platforms— validates error on unsupported complete_platformstest_uv_resolver_strict_requires_single_python— validates error on multi-version stricttest_uv_resolver_rejects_overrides— validates error on unsupported overridestest_uv_resolver_strict_generates_valid_lockfile— E2E: generates valid PEX lockfile with uv + stricttest_uv_resolver_universal_generates_valid_lockfile— E2E: generates valid PEX lockfile with uv + universalAI disclosure
This PR was created with extensive use of Claude Code (Anthropic's CLI agent). Claude explored the Pants codebase, designed the implementation approach, wrote the code/tests/docs, and ran the benchmarks. All changes were reviewed and test runs verified by the author.
🤖 Generated with Claude Code