Skip to content

Conversation

@edmorley
Copy link
Member

@edmorley edmorley commented May 7, 2025

Adds support for the package manager uv:
https://docs.astral.sh/uv/

...which has (very justifiably!) become extremely popular in the last year:

uv-star-history-2025-05-07

This specifically adds support for uv's project mode (uv sync), which uses a lockfile rather then a requirements.txt file:
https://docs.astral.sh/uv/guides/projects/

(While uv does have a pip compatible mode that supports requirements files, using a lockfile is a best practice for deploying an app, and not using one means losing most of the benefits of using uv.)

Apps must have a pyproject.toml, uv.lock and .python-version file (all of which are created by default by uv init followed by either uv lock or any of the other commands that sync/update the lockfile).

We don't support runtime.txt (or omitting the .python-version file) when using uv, since otherwise it leads to a much worse UX in several cases. (For example, when the runtime.txt version or buildpack default version differs from requires-python in the pyproject.toml file, particularly when requires-python happens to also match the distro Python in the underlying base image.)

For now, if a requirements.txt, Pipfile or poetry.lock are found, they will take precedence over uv.lock for backwards compatibility (currently a warning is shown, but in the future this will become an error). This means users of the third-party heroku-buildpack-uv will need to remove that buildpack in order to use the new native uv support, since it exports a requirements.txt file during the build which will take precedence.

For standard (non-CI) Heroku builds, the buildpack installs dependencies using uv sync --locked --no-default-groups, to ensure development/test dependencies are not installed.

We don't use --no-dev instead, since that only excludes the dev group, and some apps may be using other group names and have added them to uv's default-groups in pyproject.toml so that local development workflows work with a plain uv sync with no additional args. (PEP 735 doesn't actually prescribe recommended group names, and in fact uses test in its example rather than dev, so non-standard names may be common.)

For Heroku CI builds, the buildpack installs dependencies using uv sync --locked, which will install all default groups.

For more on uv's dependency group behaviour, see:
https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups

For now, we don't expose user provided env vars to uv sync (similar to the behaviour for other package managers), however, will start doing so as part of addressing #1451 and #1700.

uv is installed into the build cache rather than the slug, so is not available at run-time (since it's not typically needed at run-time and doing so reduces the slug size). Both Python and the entrypoints of installed dependencies are available on PATH, so use of uv run is not required at run-time to use dependencies in the environment.

Closes #1616.
GUS-W-17310796.


Note: This repository is for the classic Python buildpack used by Cedar-generation apps on Heroku. For uv support for Fir-Generation apps, see the Python CNB's tracking issue instead.

@edmorley edmorley self-assigned this May 7, 2025
@edmorley edmorley marked this pull request as ready for review May 7, 2025 15:39
@edmorley edmorley requested a review from a team as a code owner May 7, 2025 15:39
@edmorley
Copy link
Member Author

edmorley commented May 7, 2025

Example of using this with the Python Getting Started guide repo (with a demo branch that has had it's requirements.txt replaced with a pyproject.toml and uv.lock)...

$ git clone --branch edmorley/uv-demo https://github.com/heroku/python-getting-started && cd python-getting-started
Cloning into 'python-getting-started'...
...
Resolving deltas: 100% (381/381), done.

$ heroku create --buildpack 'https://github.com/heroku/heroku-buildpack-python#uv-support'
Creating app... done, ⬢ mysterious-fjord-13174
Setting buildpack to https://github.com/heroku/heroku-buildpack-python#uv-support... done
https://mysterious-fjord-13174-90ef5e4473c3.herokuapp.com/ | https://git.heroku.com/mysterious-fjord-13174.git

$ git push heroku HEAD:main
...
remote: -----> Building on the Heroku-24 stack
remote: -----> Using buildpack: https://github.com/heroku/heroku-buildpack-python#uv-support
remote: -----> Python app detected
remote: -----> Using Python 3.13 specified in .python-version
remote: -----> Installing Python 3.13.3
remote: -----> Installing uv 0.7.2
remote: -----> Installing dependencies using 'uv sync --locked --no-default-groups'
remote:        Resolved 11 packages in 0.53ms
remote:        Downloading django (7.9MiB)
remote:        Downloading brotli (2.8MiB)
remote:         Downloading brotli
remote:         Downloading django
remote:        Prepared 9 packages in 499ms
remote:        Installed 9 packages in 360ms
remote:        Bytecode compiled 979 files in 695ms
remote:         + asgiref==3.8.1
remote:         + brotli==1.1.0
remote:         + dj-database-url==2.3.0
remote:         + django==5.2.1
remote:         + gunicorn==23.0.0
remote:         + packaging==25.0
remote:         + sqlparse==0.5.3
remote:         + typing-extensions==4.13.2
remote:         + whitenoise==6.9.0
remote: -----> $ python manage.py collectstatic --noinput
remote:        WARNING:root:No DATABASE_URL environment variable set, and so no databases setup
remote:        1 static file copied to '/tmp/build_9566b80e/staticfiles', 1 post-processed.
remote:
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 24.1M
remote: -----> Launching...
remote:        Released v3
remote:        https://mysterious-fjord-13174-90ef5e4473c3.herokuapp.com/ deployed to Heroku

$ curl -sSf https://mysterious-fjord-13174-90ef5e4473c3.herokuapp.com/ | head -n 4
<!DOCTYPE html>
<html>
<head>
  <title>Python Getting Started on Heroku</title>

Copy link

@runesoerensen runesoerensen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

Adds support for the package manager uv:
https://docs.astral.sh/uv/

And specifically uv's project mode (`uv sync`), which uses a lockfile
rather then a `requirements.txt` file:
https://docs.astral.sh/uv/guides/projects/

(While uv does have a pip compatible mode that supports requirements
files, using a lockfile is a best practice for deploying an app, and not
using one means losing most of the benefits of using uv.)

Apps must have a `pyproject.toml`, `uv.lock` and `.python-version` file
(all of which are created by default by `uv init` followed by either
`uv lock` or any of the other commands that sync/update the lockfile).

We don't support `runtime.txt` (or omitting the `.python-version` file)
when using uv, since otherwise it leads to a much worse UX in several
cases. (For example, when the `runtime.txt` version or buildpack default
version differs from `requires-python` in the `pyproject.toml` file. Or
worse, when `requires-python` happens to match the distro Python in the
underlying base image.)

For now, if a `requirements.txt`, `Pipfile` or `poetry.lock` are found,
they will take precedence over `uv.lock` for backwards compatibility
(currently a warning is shown, but in the future this will become an error).
This means users of the third-party `heroku-buildpack-uv` will need to
remove that buildpack in order to use the new native uv support, since it
exports a `requirements.txt` file during the build which will take precedence.

For standard (non-Heroku CI) builds, the buildpack installs dependencies
using `uv sync --locked --no-default-groups`, to ensure development/test
dependencies are not installed.

We don't use `--no-dev` instead, since that only excludes the `dev`
group, and some apps may be using other group names and have added them
to uv's `default-groups` in `pyproject.toml` so that local development
workflows work with a plain `uv sync` with no additional args. (PEP 735
doesn't actually prescribe recommended group names, and in fact uses
`test` in its example rather than `dev`, so non-standard names may be
common.)

For Heroku CI builds, the buildpack installs dependencies using
`uv sync --locked`, which will install all default groups. See:
https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups

For now, we don't expose user provided env vars to `uv sync` (similar to
the behaviour for other package managers), however, will start doing so
as part of addressing #1451 and #1700.

uv is installed into the build cache rather than the slug, so is not
available at run-time (since it's not typically needed at run-time and
doing so reduces the slug size). Both Python and the entrypoints of
installed dependencies are available on `PATH`, so use of `uv run` is
not required at run-time to use dependencies in the environment.

Closes #1616.
GUS-W-17310796.
@edmorley edmorley merged commit 9a5a300 into main May 13, 2025
6 checks passed
@edmorley edmorley deleted the uv-support branch May 13, 2025 20:22
@heroku-linguist heroku-linguist bot mentioned this pull request May 13, 2025
@edmorley edmorley linked an issue May 13, 2025 that may be closed by this pull request
@edmorley
Copy link
Member Author

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.

Support for uv, the Python package manager Support the uv package manager

3 participants