Skip to content

Commit 8b4e277

Browse files
committed
Adopt uv as package manager
1 parent 8316bdc commit 8b4e277

25 files changed

+347
-277
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ and then editing the results to include your name, email, and various configurat
8484

8585
First, get Cookiecutter. Trust me, it's awesome:
8686

87-
$ pip install "cookiecutter>=1.7.0"
87+
$ uv tool install "cookiecutter>=1.7.0"
8888

8989
Now run it against this repo:
9090

91-
$ cookiecutter https://github.com/cookiecutter/cookiecutter-django
91+
$ uvx cookiecutter https://github.com/cookiecutter/cookiecutter-django
9292

9393
You'll be prompted for some values. Provide them, then a Django project will be created for you.
9494

docs/2-local-development/developing-locally-docker.rst

+21-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ This can take a while, especially the first time you run this particular command
3636

3737
Generally, if you want to emulate production environment use ``docker-compose.production.yml`` instead. And this is true for any other actions you might need to perform: whenever a switch is required, just do it!
3838

39+
After we have created our initial image we nee to generate a lockfile for our dependencies.
40+
Docker cannot write to the host system during builds, so we have to run the command to generate the lockfile in the container.
41+
This is important for reproducible builds and to ensure that the dependencies are installed correctly in the container.
42+
Updating the lockfile manually is normally not necessary when you add packages through `uv add <package_name>`.
43+
44+
$ docker compose -f docker-compose.local.yml run --rm django uv lock
45+
46+
This is done by running the following command: ::
47+
48+
$ docker compose -f docker-compose.local.yml run --rm django uv lock
49+
50+
To be sure we are on the right track we need to build our image again: ::
51+
52+
$ docker compose -f docker-compose.local.yml build
53+
54+
55+
56+
3957
Before doing any git commit, `pre-commit`_ should be installed globally on your local machine, and then::
4058

4159
$ git init
@@ -154,10 +172,10 @@ This tells our computer that all future commands are specifically for the dev1 m
154172
Add 3rd party python packages
155173
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
156174

157-
To install a new 3rd party python package, you cannot use ``pip install <package_name>``, that would only add the package to the container. The container is ephemeral, so that new library won't be persisted if you run another container. Instead, you should modify the Docker image:
158-
You have to modify the relevant requirement file: base, local or production by adding: ::
175+
To install a new 3rd party python package, you cannot use ``uv add <package_name>``, that would only add the package to the container. The container is ephemeral, so that new library won't be persisted if you run another container. Instead, you should modify the Docker image:
176+
You have to modify pyproject.toml and either add it to project.dependencies or to tool.uv.dev-dependencies by adding: ::
159177

160-
<package_name>==<package_version>
178+
"<package_name>==<package_version>"
161179

162180
To get this change picked up, you'll need to rebuild the image(s) and restart the running container: ::
163181

docs/2-local-development/developing-locally.rst

+10-20
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,27 @@
11
Getting Up and Running Locally
22
==============================
33

4-
.. index:: pip, virtualenv, PostgreSQL
4+
.. index:: PostgreSQL
55

66

77
Setting Up Development Environment
88
----------------------------------
99

1010
Make sure to have the following on your host:
1111

12-
* Python 3.12
12+
* uv https://docs.astral.sh/uv/getting-started/installation/
1313
* PostgreSQL_.
1414
* Redis_, if using Celery
1515
* Cookiecutter_
1616

17-
First things first.
18-
19-
#. Create a virtualenv: ::
20-
21-
$ python3.12 -m venv <virtual env path>
22-
23-
#. Activate the virtualenv you have just created: ::
24-
25-
$ source <virtual env path>/bin/activate
26-
2717
#. .. include:: generate-project-block.rst
2818

2919
#. Install development requirements: ::
3020

3121
$ cd <what you have entered as the project_slug at setup stage>
32-
$ pip install -r requirements/local.txt
22+
$ uv sync
3323
$ git init # A git repo is required for pre-commit to install
34-
$ pre-commit install
24+
$ uv run pre-commit install
3525

3626
.. note::
3727

@@ -71,15 +61,15 @@ First things first.
7161

7262
#. Apply migrations: ::
7363

74-
$ python manage.py migrate
64+
$ uv run python manage.py migrate
7565

7666
#. If you're running synchronously, see the application being served through Django development server: ::
7767

78-
$ python manage.py runserver 0.0.0.0:8000
68+
$ uv run python manage.py runserver 0.0.0.0:8000
7969

8070
or if you're running asynchronously: ::
8171

82-
$ uvicorn config.asgi:application --host 0.0.0.0 --reload --reload-include '*.html'
72+
$ uv run uvicorn config.asgi:application --host 0.0.0.0 --reload --reload-include '*.html'
8373

8474
If you've opted for Webpack or Gulp as frontend pipeline, please see the :ref:`dedicated section <bare-metal-webpack-gulp>` below.
8575

@@ -136,7 +126,7 @@ Following this structured approach, here's how to add a new app:
136126

137127
#. **Create the app** using Django's ``startapp`` command, replacing ``<name-of-the-app>`` with your desired app name: ::
138128

139-
$ python manage.py startapp <name-of-the-app>
129+
$ uv run python manage.py startapp <name-of-the-app>
140130

141131
#. **Move the app** to the Django Project Root, maintaining the project's two-tier structure: ::
142132

@@ -203,14 +193,14 @@ Next, make sure `redis-server` is installed (per the `Getting started with Redis
203193

204194
Start the Celery worker by running the following command in another terminal::
205195

206-
$ celery -A config.celery_app worker --loglevel=info
196+
$ uv run celery -A config.celery_app worker --loglevel=info
207197

208198
That Celery worker should be running whenever your app is running, typically as a background process,
209199
so that it can pick up any tasks that get queued. Learn more from the `Celery Workers Guide`_.
210200

211201
The project comes with a simple task for manual testing purposes, inside `<project_slug>/users/tasks.py`. To queue that task locally, start the Django shell, import the task, and call `delay()` on it::
212202

213-
$ python manage.py shell
203+
$ uv run python manage.py shell
214204
>> from <project_slug>.users.tasks import get_users_count
215205
>> get_users_count.delay()
216206

docs/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# You can set these variables from the command line.
55
SPHINXOPTS =
6-
SPHINXBUILD = sphinx-build
6+
SPHINXBUILD = uv run sphinx-build
77
SOURCEDIR = .
88
BUILDDIR = _build
99

hooks/post_gen_project.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def remove_utility_files():
7676

7777

7878
def remove_heroku_files():
79-
file_names = ["Procfile", "requirements.txt"]
79+
file_names = ["Procfile"]
8080
for file_name in file_names:
8181
if file_name == "requirements.txt" and "{{ cookiecutter.ci_tool }}".lower() == "travis":
8282
# don't remove the file if we are using travisci but not using heroku
@@ -188,20 +188,24 @@ def handle_js_runner(choice, use_docker, use_async):
188188

189189

190190
def remove_prettier_pre_commit():
191-
pre_commit_yaml = Path(".pre-commit-config.yaml")
192-
content = pre_commit_yaml.read_text().splitlines()
191+
remove_repo_from_pre_commit_config("mirrors-prettier")
192+
193+
194+
def remove_repo_from_pre_commit_config(repo_to_remove: str):
195+
pre_commit_config = Path(".pre-commit-config.yaml")
196+
content = pre_commit_config.read_text().splitlines(True)
193197

194198
removing = False
195199
new_lines = []
196200
for line in content:
197201
if removing and "- repo:" in line:
198202
removing = False
199-
if "mirrors-prettier" in line:
203+
if repo_to_remove in line:
200204
removing = True
201205
if not removing:
202206
new_lines.append(line)
203207

204-
pre_commit_yaml.write_text("\n".join(new_lines))
208+
pre_commit_config.write_text("\n".join(new_lines))
205209

206210

207211
def remove_celery_files():
@@ -438,6 +442,7 @@ def main():
438442

439443
if "{{ cookiecutter.use_heroku }}".lower() == "n":
440444
remove_heroku_files()
445+
remove_repo_from_pre_commit_config("uv-pre-commit")
441446

442447
if "{{ cookiecutter.use_docker }}".lower() == "n" and "{{ cookiecutter.use_heroku }}".lower() == "n":
443448
if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y":

tests/test_bare.sh

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ cd my_awesome_project
1818
sudo utility/install_os_dependencies.sh install
1919

2020
# Install Python deps
21-
pip install -r requirements/local.txt
21+
uv sync
2222

2323
# run the project's tests
24-
pytest
24+
uv run pytest
2525

2626
# Make sure the check doesn't raise any warnings
27-
python manage.py check --fail-level WARNING
27+
uv run python manage.py check --fail-level WARNING
2828

2929
# Run npm build script if package.json is present
3030
if [ -f "package.json" ]
@@ -34,4 +34,4 @@ then
3434
fi
3535

3636
# Generate the HTML for the documentation
37-
cd docs && make html
37+
cd docs && uv run make html

tests/test_cookiecutter_generation.py

+41-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import re
44
import sys
5+
import tomllib
56
from collections.abc import Iterable
67
from pathlib import Path
78

@@ -274,7 +275,7 @@ def test_djlint_check_passes(cookies, context_override):
274275
@pytest.mark.parametrize(
275276
("use_docker", "expected_test_script"),
276277
[
277-
("n", "pytest"),
278+
("n", "uv run pytest"),
278279
("y", "docker compose -f docker-compose.local.yml run django pytest"),
279280
],
280281
)
@@ -299,7 +300,7 @@ def test_travis_invokes_pytest(cookies, context, use_docker, expected_test_scrip
299300
@pytest.mark.parametrize(
300301
("use_docker", "expected_test_script"),
301302
[
302-
("n", "pytest"),
303+
("n", "uv run pytest"),
303304
("y", "docker compose -f docker-compose.local.yml run django pytest"),
304305
],
305306
)
@@ -316,7 +317,7 @@ def test_gitlab_invokes_precommit_and_pytest(cookies, context, use_docker, expec
316317
try:
317318
gitlab_config = yaml.safe_load(gitlab_yml)
318319
assert gitlab_config["precommit"]["script"] == [
319-
"pre-commit run --show-diff-on-failure --color=always --all-files",
320+
"uv run pre-commit run --show-diff-on-failure --color=always --all-files",
320321
]
321322
assert gitlab_config["pytest"]["script"] == [expected_test_script]
322323
except yaml.YAMLError as e:
@@ -326,7 +327,7 @@ def test_gitlab_invokes_precommit_and_pytest(cookies, context, use_docker, expec
326327
@pytest.mark.parametrize(
327328
("use_docker", "expected_test_script"),
328329
[
329-
("n", "pytest"),
330+
("n", "uv run pytest"),
330331
("y", "docker compose -f docker-compose.local.yml run django pytest"),
331332
],
332333
)
@@ -413,3 +414,39 @@ def test_trim_domain_email(cookies, context):
413414

414415
base_settings = result.project_path / "config" / "settings" / "base.py"
415416
assert '"me@example.com"' in base_settings.read_text()
417+
418+
419+
def test_pyproject_toml(cookies, context):
420+
author_name = "Project Author"
421+
author_email = "me@example.com"
422+
context.update(
423+
{
424+
"description": "DESCRIPTION",
425+
"domain_name": "example.com",
426+
"email": author_email,
427+
"author_name": author_name,
428+
}
429+
)
430+
result = cookies.bake(extra_context=context)
431+
assert result.exit_code == 0
432+
433+
pyproject_toml = result.project_path / "pyproject.toml"
434+
435+
data = tomllib.loads(pyproject_toml.read_text())
436+
437+
assert data
438+
assert data["project"]["authors"][0]["email"] == author_email
439+
assert data["project"]["authors"][0]["name"] == author_name
440+
assert data["project"]["name"] == context["project_slug"]
441+
442+
443+
def test_pre_commit_without_heroku(cookies, context):
444+
context.update({"use_heroku": "n"})
445+
result = cookies.bake(extra_context=context)
446+
assert result.exit_code == 0
447+
448+
pre_commit_config = result.project_path / ".pre-commit-config.yaml"
449+
450+
data = pre_commit_config.read_text()
451+
452+
assert "uv-pre-commit" not in data

tests/test_docker.sh

+19
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,38 @@
55

66
set -o errexit
77
set -x
8+
set -e
9+
10+
finish() {
11+
# Your cleanup code here
12+
docker compose -f docker-compose.local.yml down --remove-orphans
13+
docker volume rm my_awesome_project_my_awesome_project_local_postgres_data
14+
15+
}
16+
trap finish EXIT
817

918
# create a cache directory
1019
mkdir -p .cache/docker
1120
cd .cache/docker
1221

22+
sudo rm -rf my_awesome_project
23+
1324
# create the project using the default settings in cookiecutter.json
1425
uv run cookiecutter ../../ --no-input --overwrite-if-exists use_docker=y "$@"
1526
cd my_awesome_project
1627

1728
# make sure all images build
1829
docker compose -f docker-compose.local.yml build
1930

31+
docker compose -f docker-compose.local.yml run django uv lock
32+
33+
docker compose -f docker-compose.local.yml build
34+
2035
# run the project's type checks
2136
docker compose -f docker-compose.local.yml run --rm django mypy my_awesome_project
2237

38+
39+
2340
# run the project's tests
2441
docker compose -f docker-compose.local.yml run --rm django pytest
2542

@@ -44,6 +61,8 @@ docker compose -f docker-compose.local.yml run --rm \
4461
# Generate the HTML for the documentation
4562
docker compose -f docker-compose.docs.yml run --rm docs make html
4663

64+
docker build -f ./compose/production/django/Dockerfile .
65+
4766
# Run npm build script if package.json is present
4867
if [ -f "package.json" ]
4968
then

0 commit comments

Comments
 (0)