Skip to content

Commit e89d1d4

Browse files
authored
Merge pull request #72 from olivierphi/uv-test
[chore] Switching from Poetry to uv, and from Black to Ruff - big Astral commitment! 😅
2 parents 2ac5cdd + 352bf11 commit e89d1d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1916
-2818
lines changed

.dockerignore

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
/.venv
22

3+
/bin
4+
35
/node_modules
46

57
/frontend-out
68
/staticfiles
79

810
/db.sqlite3
911

10-
/src/apps/*/static/css
11-
/src/apps/*/static/js
12-
/src/apps/*/static/chess
12+
/src/apps/*/static/*/css
13+
/src/apps/*/static/*/js
1314

1415
/.git
1516
/.idea
1617
/.docker
1718

19+
*.local
20+
1821
**/.*_cache
1922
**/__pycache__
2023
**/*.pyc

.github/workflows/test-suite.yml

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,31 @@ env:
1515

1616
jobs:
1717
tests:
18-
name: "Python ${{ matrix.python-version }}"
18+
name: "Code quality and tests checks"
1919
runs-on: "ubuntu-latest"
2020

21-
strategy:
22-
matrix:
23-
python-version: ["3.11"]
24-
2521
steps:
2622
- uses: "actions/checkout@v4"
27-
- name: "Install poetry"
28-
env:
29-
POETRY_VERSION: "1.8.3"
30-
run: pipx install poetry==${POETRY_VERSION}
31-
- uses: "actions/setup-python@v5"
32-
with:
33-
python-version: "${{ matrix.python-version }}"
34-
cache: 'poetry'
35-
- name: "Install dependencies via Poetry"
36-
run: poetry install
37-
- name: "Run linting checks: Black"
38-
run: poetry run black src/
39-
- name: "Run linting checks: Ruff"
40-
run: poetry run ruff check --quiet src/
23+
- name: Set up uv
24+
# Install a specific uv version using the installer
25+
run: "curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh"
26+
env:
27+
UV_VERSION: "0.4.4"
28+
- name: Set up Python
29+
run: uv python install
30+
- name: "Install dependencies via uv"
31+
run: uv sync --all-extras
32+
- name: "Run linting checks: Ruff checker"
33+
run: uv run ruff format --check --quiet src/
34+
- name: "Run linting checks: Ruff linter"
35+
run: uv run ruff check --quiet src/
4136
- name: "Run linting checks: Mypy"
42-
run: poetry run mypy src/
37+
run: uv run mypy src/
4338
- name: "Check that Django DB migrations are up to date"
44-
run: poetry run python src/manage.py makemigrations | grep "No changes detected"
39+
run: uv run python manage.py makemigrations | grep "No changes detected"
4540
- name: "Run tests"
4641
# TODO: progressively increase minimum coverage to something closer to 80%
47-
run: poetry run pytest --cov=src --cov-report xml:coverage.xml
42+
run: uv run pytest --cov=src --cov-report xml:coverage.xml
4843
# --cov-fail-under=60 --> we'll actually do that with the "Report coverage" step
4944
- name: "Report coverage"
5045
# @link https://github.com/orgoro/coverage

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99

1010
/.venv/
1111

12+
# uv-related stuff:
13+
/bin/uv
14+
/bin/uvx
15+
/.uv.env
16+
/.uv.env.fish
17+
1218
/.docker/*
1319
!/.docker/.gitkeep
1420

.pre-commit-config.yaml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
# See https://pre-commit.com for more information
22
# See https://pre-commit.com/hooks.html for more hooks
33
repos:
4-
- repo: https://github.com/psf/black
5-
rev: 24.1.1
6-
hooks:
7-
- id: black
84
- repo: https://github.com/pre-commit/mirrors-mypy
9-
rev: v1.8.0
5+
rev: v1.11.2
106
hooks:
117
- id: mypy
128
additional_dependencies: [types-requests==2.31.0.2]
139
exclude: "^scripts/load_testing/.*\\.py$"
1410
- repo: https://github.com/charliermarsh/ruff-pre-commit
15-
rev: v0.1.14
11+
rev: v0.6.3
1612
hooks:
13+
# Run the formatter.
14+
- id: ruff-format
15+
# Run the linter.
1716
- id: ruff
1817
args: ["--fix"]
1918
exclude: "^src/project/settings/.*\\.py$"
2019
- repo: https://github.com/abravalheri/validate-pyproject
21-
rev: v0.16
20+
rev: v0.19
2221
hooks:
2322
- id: validate-pyproject

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

Dockerfile

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -53,34 +53,63 @@ RUN make frontend/img frontend/js/compile frontend/css/compile \
5353

5454
#########################################################################
5555
# Backend stuff
56+
# Large chunks copy-pasted from https://hynek.me/articles/docker-uv/ :-)
5657

5758
FROM python:3.11-slim-bookworm AS backend_build
5859

59-
ENV POETRY_VERSION=1.8.3
60-
61-
ENV DEBIAN_FRONTEND=noninteractive
62-
ENV PYTHONDONTWRITEBYTECODE=0 PYTHONUNBUFFERED=1
63-
64-
RUN apt-get update && apt-get install -y \
65-
python3-pip \
66-
python3-venv \
67-
python3-dev \
68-
python3-setuptools \
69-
python3-wheel \
70-
libpq-dev \
71-
&& apt-get clean && rm -rf /var/lib/apt/lists/*
72-
73-
RUN pip install --upgrade pip
74-
RUN pip install poetry==${POETRY_VERSION}
60+
# The following does not work in Podman unless you build in Docker
61+
# compatibility mode: <https://github.com/containers/podman/issues/8477>
62+
# You can manually prepend every RUN script with `set -ex` too.
63+
SHELL ["sh", "-exc"]
64+
65+
RUN <<EOT
66+
apt-get update -qy
67+
apt-get install -qyy \
68+
-o APT::Install-Recommends=false \
69+
-o APT::Install-Suggests=false \
70+
build-essential \
71+
ca-certificates \
72+
curl
73+
EOT
74+
75+
# Install uv.
76+
# https://docs.astral.sh/uv/guides/integration/docker/
77+
COPY --from=ghcr.io/astral-sh/uv:0.4.4 /uv /usr/local/bin/uv
7578

7679
RUN mkdir -p /app
7780
WORKDIR /app
7881

79-
RUN python -m venv --symlinks .venv
80-
81-
COPY pyproject.toml poetry.lock ./
82-
RUN poetry install --only=main --no-root --no-interaction --no-ansi
83-
82+
# - Silence uv complaining about not being able to use hard links,
83+
# - tell uv to byte-compile packages for faster application startups,
84+
# - prevent uv from accidentally downloading isolated Python builds,
85+
# - pick a Python,
86+
# - and finally declare `/app/.venv` as the target for `uv sync`.
87+
ENV UV_LINK_MODE=copy \
88+
UV_COMPILE_BYTECODE=1 \
89+
UV_PYTHON_DOWNLOADS=never \
90+
UV_PYTHON=python3.11 \
91+
UV_PROJECT_ENVIRONMENT=/app/.venv
92+
93+
# Prepare a virtual environment.
94+
# This is cached until the Python version changes above.
95+
RUN --mount=type=cache,target=/root/.cache \
96+
uv venv $UV_PROJECT_ENVIRONMENT
97+
98+
# Synchronize DEPENDENCIES without the application itself.
99+
# This layer is cached until uv.lock or pyproject.toml change.
100+
# Since there's no point in shipping lock files, we move them
101+
# into a directory that is NOT copied into the runtime image.
102+
COPY pyproject.toml /_lock/
103+
COPY uv.lock /_lock/
104+
RUN --mount=type=cache,target=/root/.cache <<EOT
105+
cd /_lock
106+
uv sync \
107+
--frozen \
108+
--no-dev \
109+
--no-install-project
110+
EOT
111+
112+
84113
FROM python:3.11-slim-bookworm AS assets_download
85114

86115
# By having a separate build stage for downloading assets, we can cache them
@@ -100,21 +129,29 @@ RUN python scripts/download_assets.py
100129

101130
FROM python:3.11-slim-bookworm AS backend_run
102131

103-
ENV DEBIAN_FRONTEND=noninteractive
104-
ENV PYTHONDONTWRITEBYTECODE=0 PYTHONUNBUFFERED=1
132+
SHELL ["sh", "-exc"]
105133

106-
RUN apt-get update && apt-get install -y \
107-
libpq5 \
108-
&& apt-get clean && rm -rf /var/lib/apt/lists/*
134+
# Add the application virtualenv to search path.
135+
ENV PATH=/app/.venv/bin:$PATH
109136

110-
RUN mkdir -p /app
111-
WORKDIR /app
137+
# Allow our "src/" folder to be seen as a package root:
138+
ENV PYTHONPATH=/app/src
112139

113-
RUN addgroup -gid 1001 webapp
114-
RUN useradd --gid 1001 --uid 1001 webapp
115-
RUN chown -R 1001:1001 /app
140+
# Let's make sure that our Django app sees "/app" as its base dir, rather than
141+
# a resolved symlink in the venv. This is important for the collectstatic command.
142+
ENV DJANGO_BASE_DIR=/app
143+
144+
# Don't run our app as root.
145+
RUN <<EOT
146+
groupadd --gid 1001 app
147+
useradd --uid 1001 -g app -N app -d /app
148+
EOT
149+
150+
USER app
151+
WORKDIR /app
116152

117153
COPY --chown=1001:1001 scripts scripts
154+
COPY --chown=1001:1001 Makefile pyproject.toml manage.py LICENSE ./
118155
COPY --chown=1001:1001 src src
119156

120157
COPY --chown=1001:1001 --from=frontend_build /app/src/apps/webui/static src/apps/webui/static
@@ -127,24 +164,24 @@ COPY --chown=1001:1001 --from=assets_download /app/src/apps/chess/static/chess/j
127164
COPY --chown=1001:1001 --from=assets_download /app/src/apps/chess/static/chess/units src/apps/chess/static/chess/units
128165
COPY --chown=1001:1001 --from=assets_download /app/src/apps/chess/static/chess/symbols src/apps/chess/static/chess/symbols
129166

130-
COPY --chown=1001:1001 Makefile pyproject.toml LICENSE ./
131-
132-
ENV PATH="/app/.venv/bin:${PATH}"
133167
RUN python -V
168+
RUN python -Im site
169+
# Smoke test:
170+
RUN DJANGO_SETTINGS_MODULE=project.settings.docker_build \
171+
.venv/bin/python manage.py shell -c "from apps.daily_challenge.models import DailyChallenge"
134172

135-
USER 1001:1001
136-
137-
# Install the project's packages (defined in pyproject.toml) in editable mode:
138-
RUN pip install -e .
139-
173+
# Collect static files.
140174
RUN DJANGO_SETTINGS_MODULE=project.settings.docker_build \
141-
.venv/bin/python src/manage.py collectstatic --noinput
175+
.venv/bin/python manage.py collectstatic --noinput
142176

177+
# Ok, let's get ready to run that server!
143178
EXPOSE 8080
144179

145180
ENV DJANGO_SETTINGS_MODULE=project.settings.production
146181

147-
ENV GUNICORN_CMD_ARGS="--bind :8080 --workers 2 --max-requests 120 --max-requests-jitter 20 --timeout 8"
182+
ENV GUNICORN_CMD_ARGS="--bind 0.0.0.0:8080 --workers 2 --max-requests 120 --max-requests-jitter 20 --timeout 8"
148183

149184
RUN chmod +x scripts/start_server.sh
150-
CMD ["scripts/start_server.sh"]
185+
# See <https://hynek.me/articles/docker-signals/>.
186+
STOPSIGNAL SIGINT
187+
ENTRYPOINT ["scripts/start_server.sh"]

Makefile

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ help:
1010
@grep -P '^[.a-zA-Z/_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
1111

1212
.PHONY: install
13-
install: .venv ./node_modules ## Install the Python and frontend dependencies
13+
install: bin/uv .venv ./node_modules ## Install the Python and frontend dependencies
14+
bin/uv sync --all-extras
1415
${PYTHON_BINS}/pre-commit install
15-
${PYTHON_BINS}/poetry install
16+
${SUB_MAKE} .venv/bin/black
1617

1718
.PHONY: dev
1819
dev: .env.local db.sqlite3
@@ -60,19 +61,19 @@ test: ## Launch the pytest tests suite
6061
${PYTHON_BINS}/pytest ${pytest_opts}
6162

6263
.PHONY: code-quality/all
63-
code-quality/all: code-quality/black code-quality/ruff code-quality/mypy ## Run all our code quality tools
64+
code-quality/all: code-quality/ruff_check code-quality/ruff_lint code-quality/mypy ## Run all our code quality tools
6465

65-
.PHONY: code-quality/black
66-
code-quality/black: black_opts ?=
67-
code-quality/black: ## Automated 'a la Prettier' code formatting
68-
# @link https://black.readthedocs.io/en/stable/
69-
@${PYTHON_BINS}/black ${black_opts} src/
66+
.PHONY: code-quality/ruff_check
67+
code-quality/ruff_check: ruff_opts ?=
68+
code-quality/ruff_check: ## Automated 'a la Prettier' code formatting
69+
# @link https://docs.astral.sh/ruff/formatter/
70+
@${PYTHON_BINS}/ruff format ${ruff_opts} src/
7071

71-
.PHONY: code-quality/ruff
72-
code-quality/ruff: ruff_opts ?= --fix
73-
code-quality/ruff: ## Fast linting
74-
# @link https://mypy.readthedocs.io/en/stable/
75-
@${PYTHON_BINS}/ruff src/ ${ruff_opts}
72+
.PHONY: code-quality/ruff_lint
73+
code-quality/ruff_lint: ruff_opts ?= --fix
74+
code-quality/ruff_lint: ## Fast linting
75+
# @link https://docs.astral.sh/ruff/linter/
76+
@${PYTHON_BINS}/ruff check src/ ${ruff_opts}
7677

7778
.PHONY: code-quality/mypy
7879
code-quality/mypy: mypy_opts ?=
@@ -142,14 +143,22 @@ frontend/img/copy_assets:
142143

143144
# Here starts the "misc util targets" stuff
144145

145-
.venv: poetry_version ?= 1.8.3
146-
.venv: ## Initialises the Python virtual environment in a ".venv" folder
147-
python -m venv .venv
148-
${PYTHON_BINS}/pip install -U pip poetry==${poetry_version}
146+
bin/uv: uv_version ?= 0.4.4
147+
bin/uv: # Install `uv` and `uvx` locally in the "bin/" folder
148+
curl -LsSf "https://astral.sh/uv/${uv_version}/install.sh" | \
149+
CARGO_DIST_FORCE_INSTALL_DIR="$$(pwd)" INSTALLER_NO_MODIFY_PATH=1 sh
150+
@echo "We'll use 'bin/uv' to manage Python dependencies."
151+
152+
.venv: ## Initialises the Python virtual environment in a ".venv" folder, via uv
153+
bin/uv venv
149154

150155
.env.local:
151156
cp .env.dist .env.local
152157

158+
.venv/bin/black: .venv ## A simple and stupid shim to use the IDE's Black integration with Ruff
159+
@echo '#!/usr/bin/env sh\n$$(dirname "$$0")/ruff format $$@' > ${PYTHON_BINS}/black
160+
@chmod +x ${PYTHON_BINS}/black
161+
153162
db.sqlite3: dotenv_file ?= .env.local
154163
db.sqlite3: ## Initialises the SQLite database
155164
touch db.sqlite3
@@ -164,7 +173,7 @@ django/manage: cmd ?= --help
164173
django/manage: .venv .env.local ## Run a Django management command
165174
@DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} ${env_vars} \
166175
${PYTHON_BINS}/dotenv -f '${dotenv_file}' run -- \
167-
${PYTHON} src/manage.py ${cmd}
176+
${PYTHON} manage.py ${cmd}
168177

169178
./.venv/bin/django: .venv install
170179

@@ -247,13 +256,13 @@ docker/local/shell:
247256
-u ${user_id} \
248257
${docker_env} ${docker_args} \
249258
-e DJANGO_SETTINGS_MODULE=project.settings.production \
250-
${DOCKER_IMG_NAME}:${DOCKER_TAG} \
251-
${cmd}
259+
--entrypoint ${cmd} \
260+
${DOCKER_IMG_NAME}:${DOCKER_TAG}
252261

253262
.PHONY: docker/local/migrate
254263
docker/local/migrate:
255264
${SUB_MAKE} docker/local/shell \
256-
cmd='/app/.venv/bin/python src/manage.py migrate'
265+
cmd='/app/.venv/bin/python manage.py migrate'
257266

258267
# Here starts Fly.io-related stuff
259268
.PHONY: fly.io/deploy

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ The game lives at [zakuchess.com](https://zakuchess.com).
3939

4040
### Running locally
4141

42-
Make sure you have Python 3.11 installed, as well as Node.js v18.
43-
44-
We recommend [pyenv](https://github.com/pyenv/pyenv-installer#readme) and [nvm](https://github.com/nvm-sh/nvm#readme) to handle specific versions of Python and Node.js,
45-
but you can of course use whatever you want :-)
42+
- Python: Entirely managed by [uv](https://docs.astral.sh/uv/). The `make install` command will
43+
make sure that the right version of `uv` is installed in the "bin/" folder - which is not ignored by git.
44+
- Node.js: Make sure you have Node.js v18 installed.
45+
We recommend [nvm](https://github.com/nvm-sh/nvm#readme) to handle specific versions of Python and Node.js,
46+
but you can of course use whatever you want :-)
4647

4748
```bash
4849
$ python -V

0 commit comments

Comments
 (0)