Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ with pip, either by `Pipfile.lock`, `requirements.txt` or `setup.py`.
You can define which python version you want using one of the following methods (in priority order):

1. **Pipfile.lock** - if present, the `python_version` field in `_meta.requires` will be used (highest priority to avoid pipenv conflicts)
2. **PYTHON_VERSION environment variable** - set via `tsuru env-set`
3. **.python-version file** - a file in your project root containing the version number
4. **Default version** - if none of the above are specified, the latest Python 3.x version will be used
2. **poetry.lock** - if present, the `python-versions` field in `[metadata]` will be used
3. **uv.lock** - if present, the `requires-python` field will be used
4. **PYTHON_VERSION environment variable** - set via `tsuru env-set`
5. **.python-version file** - a file in your project root containing the version number
6. **Default version** - if none of the above are specified, the latest Python 3.x version will be used

Always use full version numbers (e.g., `3.14.2`) or partial versions (e.g., `3.14` for latest 3.14.x, or `3.14.x`).

Expand Down Expand Up @@ -42,8 +44,8 @@ customize this behavior, see the next section for more details.
## Code deployment with dependencies

There are several ways to list the applications dependencies: `poetry.lock`,
`Pipfile.lock`, `requirements.txt` or `setup.py`. The priority order is:
poetry.lock -> Pipfile.lock -> requirements.txt -> setup.py. The file should
`uv.lock`, `Pipfile.lock`, `requirements.txt` or `setup.py`. The priority order is:
poetry.lock -> uv.lock -> Pipfile.lock -> requirements.txt -> setup.py. The file should
be in the root of deploy files.

### Using poetry.lock
Expand Down Expand Up @@ -73,6 +75,35 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
```

### Using uv.lock

If you have a `uv.lock` file (generated by [uv](https://docs.astral.sh/uv/)), tsuru will use uv
to install the dependencies of your application. uv will be installed
automatically during deployment.

You can optionally specify a uv version by setting the `PYTHON_UV_VERSION`
environment variable (e.g., `PYTHON_UV_VERSION=0.7.0` or
`PYTHON_UV_VERSION=">=0.6,<1.0"`).

uv natively supports `UV_INDEX_URL` and `UV_EXTRA_INDEX_URL` environment
variables for custom package indexes.

Example `pyproject.toml`:
```toml
[project]
name = "my-app"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"Flask>=3.0.0",
"gunicorn>=21.2.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

### Using Pipfile.lock

If you have a `Pipfile.lock` file, tsuru will use pipenv to install the
Expand Down Expand Up @@ -111,5 +142,6 @@ setup(
```

After invoking `tsuru app-deploy`, tsuru will receive your code and tell the
platform to install all the dependencies using `poetry install`, `pipenv install
--system --deploy`, `pip install -r requirements.txt` or `pip install -e ./`.
platform to install all the dependencies using `poetry install`, `uv sync`,
`pipenv install --system --deploy`, `pip install -r requirements.txt` or
`pip install -e ./`.
42 changes: 40 additions & 2 deletions python/deploy
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,20 @@ if [ -f "${CURRENT_DIR}/poetry.lock" ]; then
fi
fi

# Set version based on priority: Pipfile.lock > poetry.lock > PYTHON_VERSION env var > .python-version file > default
# Pipfile.lock and poetry.lock have highest priority because they will fail if versions don't match
# Check if uv.lock specifies a Python version
UV_PYTHON_VERSION=""
if [ -f "${CURRENT_DIR}/uv.lock" ]; then
UV_PYTHON_VERSION=$(awk '/^requires-python/ {print; exit}' "${CURRENT_DIR}/uv.lock" | sed 's/requires-python = "\(.*\)"/\1/' 2>/dev/null || echo "")

if [ -n "${UV_PYTHON_VERSION}" ]; then
# Handle various version specifiers: >=3.10, ~=3.10, ==3.10.*, etc.
# Extract the version number, removing constraint operators
UV_PYTHON_VERSION=$(echo "${UV_PYTHON_VERSION}" | sed -E 's/[\^~>=<!=]//g' | sed -E 's/\*$//' | sed -E 's/,.*$//' | xargs)
fi
fi

# Set version based on priority: Pipfile.lock > poetry.lock > uv.lock > PYTHON_VERSION env var > .python-version file > default
# Lockfiles have highest priority because they will fail if versions don't match
# Warn if both lockfile and PYTHON_VERSION are set and specify different versions
if [ -n "${PIPFILE_PYTHON_VERSION}" ] && [ -n "${PYTHON_VERSION}" ] && [ "${PIPFILE_PYTHON_VERSION}" != "${PYTHON_VERSION}" ]; then
>&2 echo "WARNING: PYTHON_VERSION environment variable (${PYTHON_VERSION}) differs from Pipfile.lock python_version (${PIPFILE_PYTHON_VERSION})."
Expand All @@ -65,13 +77,20 @@ if [ -n "${POETRY_PYTHON_VERSION}" ] && [ -n "${PYTHON_VERSION}" ] && ! [[ "${PY
>&2 echo "WARNING: PYTHON_VERSION environment variable (${PYTHON_VERSION}) differs from poetry.lock python_version (${POETRY_PYTHON_VERSION})."
>&2 echo " poetry.lock takes precedence. Update poetry.lock or unset PYTHON_VERSION to silence this warning."
fi
if [ -n "${UV_PYTHON_VERSION}" ] && [ -n "${PYTHON_VERSION}" ] && ! [[ "${PYTHON_VERSION}" =~ ^${UV_PYTHON_VERSION} ]]; then
>&2 echo "WARNING: PYTHON_VERSION environment variable (${PYTHON_VERSION}) differs from uv.lock requires-python (${UV_PYTHON_VERSION})."
>&2 echo " uv.lock takes precedence. Update uv.lock or unset PYTHON_VERSION to silence this warning."
fi

if [ -n "${PIPFILE_PYTHON_VERSION}" ]; then
PYTHON_VERSION="${PIPFILE_PYTHON_VERSION}"
VERSION_ORIGIN="Pipfile.lock"
elif [ -n "${POETRY_PYTHON_VERSION}" ]; then
PYTHON_VERSION="${POETRY_PYTHON_VERSION}"
VERSION_ORIGIN="poetry.lock"
elif [ -n "${UV_PYTHON_VERSION}" ]; then
PYTHON_VERSION="${UV_PYTHON_VERSION}"
VERSION_ORIGIN="uv.lock"
elif [ -n "${PYTHON_VERSION}" ]; then
VERSION_ORIGIN="PYTHON_VERSION environment variable"
elif [ -f "${CURRENT_DIR}/.python-version" ]; then
Expand Down Expand Up @@ -230,6 +249,25 @@ if [ -f "${CURRENT_DIR}/poetry.lock" ]; then
poetry lock --no-interaction
fi
poetry install --no-interaction --no-ansi --no-root
elif [ -f "${CURRENT_DIR}/uv.lock" ]; then
echo "uv.lock detected, using 'uv sync' to install dependencies"

UV_VERSION_SPEC=""
if [[ "${PYTHON_UV_VERSION}" != "" ]]; then
UV_VERSION_SPEC="${PYTHON_UV_VERSION}"
if [[ "$PYTHON_UV_VERSION" =~ ^[0-9] ]]; then
UV_VERSION_SPEC="==${PYTHON_UV_VERSION}"
fi
echo "Using uv version ${UV_VERSION_SPEC}"
else
echo "Using latest uv version"
fi
pip install uv${UV_VERSION_SPEC}

# Configure custom UV index from environment variables
# UV_INDEX_URL and UV_EXTRA_INDEX_URL are natively supported by uv

uv sync --no-dev --frozen --no-install-project --python-preference only-system
elif [ -f "${CURRENT_DIR}/Pipfile.lock" ]; then
echo "Pipfile.lock detected, using 'pipenv install --system --deploy' to install dependencies"

Expand Down
119 changes: 119 additions & 0 deletions tests/python/tests.bats
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,125 @@ EOF
unset PYTHON_VERSION
}

@test "install from uv.lock" {
unset PYTHON_VERSION
cp uv_pyproject.toml ${CURRENT_DIR}/pyproject.toml
cp uv.lock ${CURRENT_DIR}/

run /var/lib/tsuru/deploy
assert_success

[[ "$output" == *"uv.lock detected"* ]]
[[ "$output" == *"Using latest uv version"* ]]

pushd ${CURRENT_DIR}
run python --version
popd

assert_success
[[ "$output" == *"3."* ]]

pushd ${CURRENT_DIR}
run uv pip freeze --python .venv/bin/python
popd

assert_success
[[ "$output" == *"msgpack"* ]]
rm -rf ${CURRENT_DIR}/pyproject.toml ${CURRENT_DIR}/uv.lock ${CURRENT_DIR}/.venv
}

@test "install from uv.lock with custom uv version" {
unset PYTHON_VERSION
export PYTHON_UV_VERSION=0.7.0
cp uv_pyproject.toml ${CURRENT_DIR}/pyproject.toml
cp uv.lock ${CURRENT_DIR}/

run /var/lib/tsuru/deploy
assert_success
[[ "$output" == *"Using uv version ==0.7.0"* ]]

run uv version
assert_success
[[ "$output" == *"0.7.0"* ]]

pushd ${CURRENT_DIR}
run uv pip freeze --python .venv/bin/python
popd

assert_success
[[ "$output" == *"msgpack"* ]]

rm -rf ${CURRENT_DIR}/pyproject.toml ${CURRENT_DIR}/uv.lock ${CURRENT_DIR}/.venv
unset PYTHON_UV_VERSION
}

@test "uv.lock takes precedence over requirements.txt" {
unset PYTHON_VERSION
cp uv_pyproject.toml ${CURRENT_DIR}/pyproject.toml
cp uv.lock ${CURRENT_DIR}/
echo "alf==0.7.0" > ${CURRENT_DIR}/requirements.txt

run /var/lib/tsuru/deploy
assert_success

[[ "$output" == *"uv.lock detected"* ]]
[[ "$output" != *"requirements.txt detected"* ]]

pushd ${CURRENT_DIR}
run uv pip freeze --python .venv/bin/python
popd

assert_success
[[ "$output" == *"msgpack"* ]]
[[ "$output" != *"alf"* ]]

rm -rf ${CURRENT_DIR}/pyproject.toml ${CURRENT_DIR}/uv.lock ${CURRENT_DIR}/requirements.txt ${CURRENT_DIR}/.venv
}

@test "poetry.lock takes precedence over uv.lock" {
unset PYTHON_VERSION
cp pyproject.toml poetry.lock ${CURRENT_DIR}/
cp uv.lock ${CURRENT_DIR}/

run /var/lib/tsuru/deploy
assert_success

[[ "$output" == *"poetry.lock detected"* ]]
[[ "$output" != *"uv.lock detected"* ]]

rm -rf ${CURRENT_DIR}/pyproject.toml ${CURRENT_DIR}/poetry.lock ${CURRENT_DIR}/uv.lock
}

@test "uv.lock python version takes precedence over PYTHON_VERSION" {
export PYTHON_VERSION=3.9.15
cp uv_pyproject.toml ${CURRENT_DIR}/pyproject.toml
cp uv.lock ${CURRENT_DIR}/

run /var/lib/tsuru/deploy
assert_success

[[ "$output" == *"uv.lock detected"* ]]
[[ "$output" == *"Using python version: 3.10"* ]]
[[ "$output" == *"(uv.lock)"* ]]

pushd ${CURRENT_DIR}
run python --version
popd

assert_success
[[ "$output" == *"3.10"* ]]

pushd ${CURRENT_DIR}
run uv pip freeze --python .venv/bin/python
popd

assert_success
[[ "$output" == *"msgpack"* ]]

rm -rf ${CURRENT_DIR}/pyproject.toml ${CURRENT_DIR}/uv.lock ${CURRENT_DIR}/.venv
unset PYTHON_VERSION
}

@test "install from poetry.lock with custom repository" {
unset PYTHON_VERSION
export POETRY_REPOSITORIES_ARTIFACTORY_URL=https://pypi.org/simple
Expand Down
Loading
Loading