Skip to content

Commit

Permalink
Merge pull request #74 from openradx/poetry-to-uv
Browse files Browse the repository at this point in the history
Switch from poetry to uv
  • Loading branch information
medihack authored Feb 15, 2025
2 parents 2bf845f + 02481e3 commit 19ee5ce
Show file tree
Hide file tree
Showing 18 changed files with 2,320 additions and 3,544 deletions.
4 changes: 1 addition & 3 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ USER root
# - gettext for Django translations
# - postgresql-common for the apt.postgresql.org.sh script
# - postgresql-client-17 for a current version of psql
# Make sure to match the version of the postgres service in the compose file!
RUN sudo apt-get update \
&& apt-get install -y --no-install-recommends \
bash-completion \
Expand All @@ -17,6 +18,3 @@ RUN sudo apt-get update \
&& rm -rf /var/lib/apt/lists/*

USER vscode

RUN pipx install poetry \
&& poetry completions bash >> ~/.bash_completion
7 changes: 4 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/node:1": {}
},
// https://github.com/orgs/community/discussions/50403
// "initializeCommand": "docker system prune --all --force",
"postCreateCommand": "poetry install && npm install && poetry run ./cli.py init-workspace",
"remoteEnv": {
"UV_CACHE_DIR": "/workspace/.cache"
},
"postCreateCommand": "uv sync && npm install && uv run ./cli.py init-workspace",
"customizations": {
"vscode": {
"extensions": [
Expand Down
2 changes: 1 addition & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"allowedVersions": "<=3.12"
},
{
"managers": ["poetry"],
"managers": ["pep621"],
"matchPackageNames": ["factory-boy"],
"allowedVersions": "<=3.3.2"
}
Expand Down
28 changes: 12 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,23 @@ jobs:
ci:
strategy:
fail-fast: false
matrix:
python-version: ["3.12"]
poetry-version: ["2.0.1"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
runs-on: "ubuntu-latest"
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.6.0"
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Setup Poetry
uses: abatilo/actions-poetry@v4
with:
poetry-version: ${{ matrix.poetry-version }}
python-version-file: ".python-version"
- name: Install dependencies
run: poetry install
run: uv sync
- name: Configure environment
run: poetry run ./cli.py init-workspace
run: uv run ./cli.py init-workspace
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and cache Docker images
Expand All @@ -41,14 +37,14 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Start Docker containers
run: poetry run ./cli.py compose-up --no-build
run: uv run ./cli.py compose-up --no-build
- name: Run linting
# https://github.com/actions/runner/issues/241#issuecomment-745902718
shell: 'script -q -e -c "bash {0}"'
run: poetry run ./cli.py lint
run: uv run ./cli.py lint
- name: Run tests
shell: 'script -q -e -c "bash {0}"'
run: poetry run ./cli.py test --cov
run: uv run ./cli.py test --cov
- name: Stop Docker containers
if: ${{ always() }}
run: poetry run ./cli.py compose-down
run: uv run ./cli.py compose-down
10 changes: 0 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,6 @@ target/
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"cSpell.diagnosticLevel": "Hint",
"docker.containers.label": "ContainerName",
"editor.formatOnSave": true,
"evenBetterToml.schema.enabled": false,
"javascript.validate.enable": true,
"js/ts.implicitProjectConfig.checkJs": true,
"notebook.formatOnSave.enabled": true,
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ It also contains a Django example project to play around with the features.
```terminal
git clone https://github.com/openradx/adit-radis-shared.git
cd adit-radis-shared
poetry install
poetry run ./cli.py compose-up
uv sync
uv run ./cli.py compose-up
```

The development server of the example project will be started on <http://localhost:8000>

If a library dependency is changed, the containers need to be rebuilt (e.g. by running
`poetry run ./cli.py compose-down && poetry run ./cli.py compose-up`).
`uv run ./cli.py compose-down && uv run ./cli.py compose-up`).
103 changes: 34 additions & 69 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,92 +1,57 @@
FROM python:3.12-bullseye AS python-base
# Ideas from https://docs.astral.sh/uv/guides/integration/docker/

# python
# ENV variables are also available in the later build stages
ENV PYTHONUNBUFFERED=1 \
# prevents python creating .pyc files
PYTHONDONTWRITEBYTECODE=1 \
\
# pip
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
\
# poetry
# https://python-poetry.org/docs/#installing-with-the-official-installer
# https://python-poetry.org/docs/configuration/#using-environment-variables
POETRY_VERSION=2.0.1 \
# make poetry install to this location
POETRY_HOME="/opt/poetry" \
# make poetry create the virtual environment in the project's root
# it gets named `.venv`
POETRY_VIRTUALENVS_IN_PROJECT=true \
# do not ask any interactive question
POETRY_NO_INTERACTION=1 \
\
# paths
# this is where our requirements + virtual environment will live
PYSETUP_PATH="/opt/pysetup" \
VENV_PATH="/opt/pysetup/.venv" \
# needed for adit-radis-shared to be found
PYTHONPATH="/app"

# prepend poetry and venv to path
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"

# deps for db management commands
# make sure to match the postgres version to the service in the compose file
FROM python:3.12-bullseye AS builder-base

# Install dependencies for the `psql` command.
# Must match the version of the postgres service in the compose file!
RUN apt-get update \
&& apt-get install --no-install-recommends -y postgresql-common \
&& /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \
&& apt-get install --no-install-recommends -y \
postgresql-client-17 \
&& rm -rf /var/lib/apt/lists/*

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

# `builder-base` stage is used to build deps + create our virtual environment
FROM python-base AS builder-base
COPY --from=ghcr.io/astral-sh/uv:0.6.0 /uv /uvx /bin/

RUN apt-get update \
&& apt-get install --no-install-recommends -y \
# deps for installing poetry
curl \
# deps for building python deps
build-essential \
&& rm -rf /var/lib/apt/lists/*
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy

# install poetry - respects $POETRY_VERSION & $POETRY_HOME
RUN curl -sSL https://install.python-poetry.org | python3 -
# There is no git during image build so we need to provide a fake version
ENV UV_DYNAMIC_VERSIONING_BYPASS=0.0.0

# copy project requirement files here to ensure they will be cached.
WORKDIR $PYSETUP_PATH
COPY poetry.lock pyproject.toml README.md ./
ENV PATH="/app/.venv/bin:$PATH"

# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally
RUN poetry install --without dev --no-root
WORKDIR /app


# `development` image is used during development / testing
FROM python-base AS development
# development image
FROM builder-base AS development

WORKDIR $PYSETUP_PATH
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project

# copy in our built poetry + venv
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
RUN playwright install --with-deps chromium

# quicker install as runtime deps are already installed
RUN poetry install --no-root
ADD . /app

# Install requirements for end-to-end testing
RUN playwright install --with-deps chromium
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen

# will become mountpoint of our code
WORKDIR /app

# production image
FROM builder-base AS production

# `production` image used for runtime
FROM python-base AS production
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
COPY . /app/
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev

WORKDIR /app
ADD . /app

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
25 changes: 12 additions & 13 deletions adit_radis_shared/cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,35 +138,34 @@ def lint():
"""Lint the source code with ruff, pyright and djlint"""

print("Linting Python code with ruff...")
helpers.execute_cmd("poetry run ruff check .")
helpers.execute_cmd("uv run ruff check .")

print("Linting Python code with pyright...")
helpers.execute_cmd("poetry run pyright")
helpers.execute_cmd("uv run pyright")

print("Linting Django templates with djlint...")
helpers.execute_cmd("poetry run djlint . --lint")
helpers.execute_cmd("uv run djlint . --lint")


def format_code():
"""Format the source code with ruff and djlint"""

print("Formatting Python code with ruff...")
helpers.execute_cmd("poetry run ruff format .")
helpers.execute_cmd("uv run ruff format .")

print("Sorting Python imports with ruff...")
helpers.execute_cmd("poetry run ruff check . --fix --select I")
helpers.execute_cmd("uv run ruff check . --fix --select I")

print("Formatting Django templates with djlint...")
helpers.execute_cmd("poetry run djlint . --reformat")
helpers.execute_cmd("uv run djlint . --reformat")


def test(ctx: typer.Context):
"""Run the test suite with pytest"""

if not helpers.check_compose_up():
sys.exit(
"Acceptance tests need dev containers running.\n"
"Run 'poetry run ./cli.py compose-up' first."
"Acceptance tests need dev containers running.\nRun 'uv run ./cli.py compose-up' first."
)

cmd = (
Expand All @@ -181,10 +180,10 @@ def show_outdated():
"""Show outdated dependencies"""

print("### Outdated Python dependencies ###")
helpers.execute_cmd("poetry show --outdated --top-level")
helpers.print_uv_outdated()

print("### Outdated NPM dependencies ###")
helpers.execute_cmd("npm outdated")
helpers.execute_cmd("npm outdated", hidden=True)


def backup_db():
Expand All @@ -202,7 +201,7 @@ def backup_db():

web_container_id = helpers.find_running_container_id("web")
if web_container_id is None:
sys.exit("Web container is not running. Run 'poetry run ./cli.py compose-up' first.")
sys.exit("Web container is not running. Run 'uv run ./cli.py compose-up' first.")

helpers.execute_cmd(
(
Expand All @@ -222,7 +221,7 @@ def restore_db():
)
web_container_id = helpers.find_running_container_id("web")
if web_container_id is None:
sys.exit("Web container is not running. Run 'poetry run ./manage.py compose_up' first.")
sys.exit("Web container is not running. Run 'uv run ./manage.py compose-up' first.")

helpers.execute_cmd(
(
Expand Down Expand Up @@ -324,7 +323,7 @@ def generate_certificate_chain(self):
if not cert_path.is_file():
sys.exit(
f"SSL certificate file {cert_path.absolute()} does not exist. You can generate an"
" unsigned certificate with 'poetry run ./manage.py generate_certificate_files'"
" unsigned certificate with 'uv run ./manage.py generate-certificate_files'"
" with included chain file. If you have a signed certificate from a CA, be sure to"
" provide the correct SSL_SERVER_CERT_FILE setting in '.env'."
)
Expand Down
23 changes: 23 additions & 0 deletions adit_radis_shared/cli_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,26 @@ def generate_chain_file_for_host(hostname: str):
response.raise_for_status()
chain_pem = response.content
return chain_pem


def print_uv_outdated():
# Run the uv tree command and capture its text output.
result = subprocess.run(
["uv", "tree", "--outdated", "--depth", "1"],
capture_output=True,
text=True,
check=True,
)
result = capture_cmd("uv tree --depth 1 --outdated")

# Sample line: "├── django v5.1.2 (latest: v5.1.3)"
pattern = re.compile(r"^[│├└─]+\s+([\w\-\._]+)\s+v([\d\.]+).*?\(latest:\s*v([\d\.]+)\)")

for line in result.splitlines():
match = pattern.search(line)
if match:
pkg_name = match.group(1)
installed = match.group(2)
latest = match.group(3)
if installed != latest:
print(f"{pkg_name}: {installed} (latest: {latest})")
7 changes: 1 addition & 6 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ x-app: &default-app
image: example_project_dev:latest
volumes:
- .:/app
- vscode-server:/root/.vscode-server/extensions
- vscode-server-insiders:/root/.vscode-server-insiders/extensions
- /app/.venv
environment:
DJANGO_INTERNAL_IPS: ${DJANGO_INTERNAL_IPS:?}
DJANGO_SETTINGS_MODULE: example_project.settings.development
Expand Down Expand Up @@ -49,7 +48,3 @@ services:
POSTGRES_PASSWORD: postgres
ports:
- ${POSTGRES_DEV_PORT:-5432}:5432

volumes:
vscode-server:
vscode-server-insiders:
2 changes: 1 addition & 1 deletion example.env
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ SITE_DOMAIN=localhost
# These variables are used to create a certificate key, self-signed certificate,
# and the corresponding certificate chain. If you have an existing certificate
# key and signed certificate from your CA, you can generate the corresponding
# certificate chain using 'poetry run ./cli.py generate-certificate-chain'.
# certificate chain using 'uv run ./cli.py generate-certificate-chain'.
SSL_HOSTNAME=localhost
SSL_IP_ADDRESSES=127.0.0.1
SSL_SERVER_CERT_FILE="./cert.pem"
Expand Down
Loading

0 comments on commit 19ee5ce

Please sign in to comment.