(POC) Investigate uv#19953
Conversation
Port the contents of requirements/*.in into a single PEP 735 [dependency-groups] table in pyproject.toml. main.in becomes the [project].dependencies; the seven other .in files become groups (deploy, dev, ipython, lint, tests, docs-dev, docs-user, docs-blog). Generate uv.lock alongside (324 packages, 1,755 SHA-256 hashes, single cross-platform lock vs the previous 8 per-arch .txt files). tool.uv.package = false marks warehouse as an application, not a library — uv sync will not try to build a wheel. Existing requirements/*.txt remain in place; they are removed in a later commit once the Dockerfile + Makefile have switched over to uv sync --frozen.
Replace the per-stage uv pip install -r requirements/*.txt calls with
uv sync --frozen reading directly from pyproject.toml + uv.lock. The
docs stage now bind-mounts pyproject.toml + uv.lock instead of the
requirements/ directory; the build stage collapses three RUN steps
(dev, ipython, main+deploy+tests+lint) into a single uv sync that
conditionally adds groups based on $DEVEL, $IPYTHON, $CI.
Add UV_PROJECT_ENVIRONMENT=/opt/warehouse so uv sync targets the venv
we created (it does not respect VIRTUAL_ENV for project syncs).
--no-default-groups disables uv's auto-installation of the dev group.
Side benefits:
- Reads URLs + SHA-256s straight from uv.lock; no PyPI index lookups
during install (resolves the warehouse-deploys-on-warehouse circular
dependency dstufft has flagged).
- Combined install drops from 18.3s (3x uv pip install) to 10.4s.
- Removes ~15 lines of Dockerfile.
requirements/*.txt remain on disk for now; bin/deps* still consume them
and will be removed in a follow-up commit.
Catches the case where someone edits pyproject.toml dependencies but forgets to regenerate uv.lock. Runs first in bin/lint so it fails fast before the longer ruff/mypy/djlint passes.
pyproject.toml + uv.lock are now the sole source of truth for Python
dependencies; the Dockerfile, Makefile, and CI cache key all read from
them directly. The requirements/ workflow (and the bin/deps,
bin/deps-upgrade, bin/depchecker.py scripts that drove it) is no
longer reachable.
Removed:
- requirements/ directory (8 .in files + 9 .txt files, ~6700 lines)
- requirements.txt (root dependabot shim)
- bin/deps, bin/deps-upgrade, bin/depchecker.py
- pip-tools and pip-api from the dev dependency group
- [tool.pip-tools.compile] from pyproject.toml
Updated:
- Makefile: deps / deps_upgrade_all / deps_upgrade_project now run
'uv lock --check' / 'uv lock --upgrade' / 'uv lock --upgrade-package'
instead of bin/deps* scripts. Pattern rule for requirements/%.txt
is gone.
- Makefile: .state/docker-build-{base,docs} dependencies updated
from requirements/*.txt to pyproject.toml + uv.lock.
- .github/workflows/ci.yml: mypy cache key hashes pyproject.toml +
uv.lock instead of requirements/*.txt.
Verified make build (no-cache) still succeeds in ~55s.
605eb90 to
78f3a53
Compare
…lock --check
The migration to pyproject.toml + uv.lock allowed uv to resolve forward
to newer versions than the previous requirements/*.txt files held; that
caused real breakage in CI's Check Database Consistency job:
AttributeError: 'SchemaPath' object has no attribute 'contents'
Triggered by openapi-core 0.22 → 0.23 + jsonschema-path 0.3 → 0.4,
which moved an attribute pyramid_openapi3 still calls.
The migration policy should be: tooling change only, no functional
dep diffs. Future bumps are deliberate per-package PRs.
This commit:
- Adds tool.uv.constraint-dependencies (299 entries) pinning every
transitive to the version it had in the deleted requirements/*.txt
files. Verified zero drift on shared packages.
- Adds tool.uv.environments = ["sys_platform == 'linux'"] to scope
the lock to the single platform we deploy to. The previous
pip-compile workflow was implicitly single-platform; uv's default
cross-platform locking surfaced win32 marker conflicts (e.g.
mkdocs-rss-plugin 1.17.9 pinning tzdata<2026 on win32) that were
silently ignored before. Re-enabling cross-platform is a follow-up.
- Updates .github/workflows/ci.yml: the Dependencies job ran bin/deps,
which the cleanup commit deleted. Replaced with uv lock --check
(the new equivalent — same drift-detection purpose).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lint The Dependencies job ran 'uv lock --check' (was 'bin/deps' pre-migration) to verify pyproject.toml/uv.lock are consistent. That same check now runs as the first line of bin/lint (added in commit b76e18f), so the Lint job already covers it. Keeping a separate matrix entry just duplicated the check and required adding uv to the runtime container or restructuring the job to run outside the container; both costs for no extra signal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bin/rtd-docs ran 'pip install -r requirements/docs-{dev,user,blog}.txt'
to install Read the Docs build deps. Those files were removed in commit
78f3a53; RTD broke for all three projects (warehouse, docspypiorg,
blogpypiorg).
Replace with:
- 'pip install uv' so uv is available in RTD's host Python (RTD does
not run our Dockerfile)
- 'uv sync --frozen --no-default-groups --group docs-X' to install
just the relevant docs group from pyproject.toml + uv.lock
- 'uv run --no-sync mkdocs build ...' so mkdocs runs in the synced
venv without re-resolving
Also update the change-detection diff filter: it watched
'requirements/docs-X.txt' for skip-if-unchanged decisions; replace with
'pyproject.toml uv.lock'. This over-triggers slightly (any dep change
busts the skip) but is the simplest approximation; finer per-group
detection would need to parse the dep groups out of pyproject.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bin/lint now runs 'uv lock --check' as its first step. The Lint CI job runs bin/lint inside the deployed runtime container, which previously had no uv on PATH (uv was only copied into the build/docs stages). Result: 'uv: command not found' / exit 127. Add the same COPY --from=ghcr.io/astral-sh/uv to the final stage. ~25MB image growth; keeps uv available for any in-container task (lint, deps upgrades, ad-hoc shell work). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… to bin/lint" This reverts the matrix removal. Dependencies is a required check in branch protection, so removing it leaves the PR's "Dependencies" status stuck on "Expected — Waiting for status to be reported" indefinitely. uv is now in the runtime stage (commit daadb0e), so the same 'uv lock --check' that bin/lint runs as its first step works fine as a standalone matrix entry too. Slight duplication, much simpler than either removing the branch protection requirement or restructuring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
we continued poking at this, to see what ci and other things would look like. CI wall-clock comparison: pre-ruff baseline → ruff PR → uv migration GitHub Actions, same workflow (
The matrix jobs run in parallel, so total wall = build + slowest matrix job (Tests). Where the time went:
Stacked: ~159 seconds saved per CI run vs pre-ruff. PRs running CI 5+ times across review iterations recover ~13 minutes of wall-clock per PR. |
| "types-zxcvbn==4.5.0.20260408", | ||
| "typing-extensions==4.15.0", | ||
| "typing-inspection==0.4.2", | ||
| "tzdata==2026.1", |
There was a problem hiding this comment.
| "tzdata==2026.1", |
If you delete this and re-run uv lock, it'll select one or two versions back and then will work on windows as well. Not the biggest deal either way, but nice to support
| name = "warehouse" | ||
| version = "0.0.0" | ||
| description = "PyPI's web application" | ||
| requires-python = ">=3.13" |
There was a problem hiding this comment.
| requires-python = ">=3.13" | |
| requires-python = "==3.13.*" |
we only support a single version at a time, so there's no reason to let other versions influence our lock file.
| "Babel", | ||
| "bcrypt", | ||
| "boto3", | ||
| "celery[redis]>=5.6.2,<5.7", |
There was a problem hiding this comment.
| "celery[redis]>=5.6.2,<5.7", | |
| "celery[redis]>=5.6.2", |
Currently we're not really using these pins, we're regularly updating the upper bounds (via dependabot) so we're tracking the latest version. With uv an upper bounds won't get automatically bumped liked that, because the lock file is what keeps you from upgrading.
| # https://github.com/jazzband/pip-tools/issues/1577 | ||
| "kombu[redis]>=5.6,<5.7", |
There was a problem hiding this comment.
| # https://github.com/jazzband/pip-tools/issues/1577 | |
| "kombu[redis]>=5.6,<5.7", |
this was added due to a pip-tools bug that uv doesn't have so there's no reason to keep this.
| "rfc3986", | ||
| "sentry-sdk", | ||
| "pypi-attestations==0.0.29", | ||
| "sqlalchemy[asyncio]>=2.0,<3.0", |
There was a problem hiding this comment.
| "sqlalchemy[asyncio]>=2.0,<3.0", | |
| "sqlalchemy[asyncio]>=2.0", |
Currently we're not really using these pins, we're regularly updating the upper bounds (via dependabot) so we're tracking the latest version. With uv an upper bounds won't get automatically bumped liked that, because the lock file is what keeps you from upgrading.
| # requirements/*.txt files. The migration to pyproject.toml + uv.lock should | ||
| # not silently bump versions; future upgrades happen via deliberate PRs that | ||
| # remove a constraint and re-lock. | ||
| constraint-dependencies = [ |
There was a problem hiding this comment.
Once the uv.lock file has been added with these constraints, you can delete this from the pyproject.toml.
Creating the uv.lock with these constraints will ensure the lock file is generated with these constraints in mind, and then once we have that uv.lock, uv will ensure that we don't change versions without an explicit upgrade happening.
| "redis>=2.8.0,<7.0.0", | ||
| "rfc3986", | ||
| "sentry-sdk", | ||
| "pypi-attestations==0.0.29", |
There was a problem hiding this comment.
| "pypi-attestations==0.0.29", | |
| "pypi-attestations", |
Currently we're not really using these pins, we're regularly updating the upper bounds (via dependabot) so we're tracking the latest version. With uv an upper bounds won't get automatically bumped liked that, because the lock file is what keeps you from upgrading.
| @exit 1 | ||
|
|
||
| .state/docker-build-base: Dockerfile package.json package-lock.json requirements/main.txt requirements/deploy.txt requirements/lint.txt requirements/tests.txt requirements/dev.txt | ||
| .state/docker-build-base: Dockerfile package.json package-lock.json pyproject.toml uv.lock |
There was a problem hiding this comment.
We probably only need this to depend on uv.lock and not pyproject.toml, the uv.lock file depends on the relevant content in pyproject.toml, but there is also irrelevant content in pyproject.toml that would needlessly trigger a rebuild.
| [project] | ||
| name = "warehouse" | ||
| version = "0.0.0" | ||
| description = "PyPI's web application" |
There was a problem hiding this comment.
| description = "PyPI's web application" |
this is a nit, but this field isn't required and there's no real benefit to specifying it that I can tell.
| @@ -1,3 +1,480 @@ | |||
| [project] | |||
| name = "warehouse" | |||
| version = "0.0.0" | |||
There was a problem hiding this comment.
| version = "0.0.0" | |
| version = "1" |
warehouse is stable 😉
| path: | | ||
| dev/.mypy_cache | ||
| key: ${{ runner.os }}-mypy-${{ env.pythonLocation }}-${{ hashFiles('requirements.txt', 'requirements/*.txt') }} | ||
| key: ${{ runner.os }}-mypy-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'uv.lock') }} |
There was a problem hiding this comment.
| key: ${{ runner.os }}-mypy-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'uv.lock') }} | |
| key: ${{ runner.os }}-mypy-${{ env.pythonLocation }}-${{ hashFiles('uv.lock') }} |
The uv.lock is enough to know if the dependencies has changed and avoids unrelated changes to pyproject.toml invalidating the cache.
|
You probably want to update the dependabot config to tell it to use uv, and the |
| set -ex | ||
|
|
||
| # Fail if pyproject.toml has changed without regenerating uv.lock. | ||
| uv lock --check |
There was a problem hiding this comment.
This is really fast, so it's probably not a big deal either way, but given make deps still exists/works, that uv itself will generally enforce this, and CI has the Dependencies matrix item still, is this providing much value here?
I'm ok either way, just asking if it is :)
| FROM python:${PYTHON_IMAGE_VERSION} AS docs | ||
|
|
||
| # Pull the uv binary in so we can use it as a faster pip / pip-compile. | ||
| COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /usr/local/bin/ |
There was a problem hiding this comment.
Can dependabot update this?
Either way, we reference the hardcoded version several times, it would be good to extract that into a variable like was done with PYTHON_IMAGE_VERSION.
|
What version of pip was this benchmarked against? I don't know where bottlenecks are in this CI but there were big improvements to metadata processing, resolver heuristics, and memory consumption in 26.1. I'm not saying to not go with uv, it has a lot of functionality pip never will, but my long term aim is performance parity. |
|
it may have been 26.0, ill retry with latest |
Very POC to see how well we could fare from having modern tooling that wasn't super slow in local development.
Test swappin pip -> uv for local dev. Could also try pixi or something else with the uv resolver backend like pdm but was most familiar with this and we did the whole ruff-ification recently.
Initial commit (4e424ba) uses same
requirements/*.txtformat, no lockfile change for now. Would prefer modern pyproject.toml + lockfile if we were to proceed. Later commits modernizes further. but you can just checkout the initial.Cold-cache, no-docker-cache benchmarks on my machine:
make deps_upgrade_all(resolve all 8 .in files)main.inalonemake buildbase image — install stepsmake builddocs image — install stepsbottleneck here is the frontend stuff, could swap to rspack
history here is pretty linear.