From: wayne+blog@waynewerner.com To: everyone.everywhere.all.at.once Date: Mon, 17 Mar 2025 12:56:29 -0500 Subject: Django Projects, Apps, Config, and You
Just some notes on how I made Django work together with my own blend of apps.
I've been a huge fan of Python for years, and lately I've been working on some projects using some tools that you may have heard of. Django and uv in particular.
I'm not intending the rest of this as a comprehensive guide, mostly as a helpful resource and a jumping off point to where you can use these terms to find more information about Django & friends.
For quite some time I've been reticent to adopt uv as part of my workflow. It's just another tool, I thought, plus Rust isn't friendly to all platforms, though I do like it. However, lately I've grown to really really love uv.
Ever since I learned how to create Python packages, I've loved making them for anything, regardless of whether or not I've published them to PyPI. Though I have published a few. And I've usually tried to follow some good patterns when building them. Generally my projects started with this basic setup:
setup.py
myproject/__init__.py
myproject/some_module.py
That worked well, especially when I learned about pip install -e
. And then
came setup.cfg so I no longer had behavior in my project definition, and now we
have pyproject.toml so not only do we get data, but TOML has a rigorous and
simple definition. At this point, life was good.
When I first came across the src/
style layout I was skeptical. Most of the
time I was just running my code via automated tests, and I didn't really see
the benefit since it was just another subdirectory in my way. However, there
were more than one time where I was bit by the
accidentally-importing-my-uninstalled package, which finally convinced me. So I
changed the above to:
pyproject.toml
src/myproject/__init__.py
src/myproject/some_module.py
My other favorite trick was using -m
, which allows you to execute a module,
and it will have __name__
== 'main', so I could guard some behavior to provide some extra tooling, like perhaps
python -m myproject.config, which could configure the application. But I could also
import myproject.config` and
not launch that behavior. In other words, I could protect some code with
if __name__ == "__main__":
do_something()
and the python -m
invocation would call that, but it wouldn't do anything
otherwise. This was pretty neat! But when I adopted uv
I would run uv venv
and then activate the virtualenv the same way. But I ran into weird issues with
the dependencies and uv that made things kind of awkward. Like maybe I'd do uv run python -m myproject.some_module
which was kind of a huge pain.
As it turns out, my workflow was actually already supported by uv! uv run -m myproject.some_module
actually worked correctly. Better yet, it would
automatically install my dependencies. And unlike with pip install -e .
where
I would have to go edit pyproject.toml
, add my dependency, and then
reinstall... I can just run, uv add DEPENDENCY
will add my dependency in
pyproject.toml as well as install it. Neat!
Another neato thing that I encountered was using I think it's called a custom
namespace. If you've ever used flask you've probably come across
flask.ext.somepkg
-- and I had tools that I was building that were really
specifically for me and not particularly useful for other folks maybe so it
made a lot of sense to keep them in my own lil' namespace. Thus, I made my
projects look like this:
myproject/pyproject.toml
myproject/src/ww/myproject/__init__.py
myproject/src/ww/myproject/cool_module.py
For general Python packages this is all well and good, but recently I revisted using Django. In the past I've typically just used Flask because I've been doing super basic sites that were just a bit of HTML and maybe hitting a database, but nothing with any advanced needs. But lately I was reintroducd to Django.
If you know anything about Django you'll know that it has projects and apps. Sure, you could package up your entire website into a single project and that would actually be entirely valid, but I love re-use probably more than is health, so the idea of splitting my Django projects out into apps that can be installed in several different projects really sat well with me.
I really wanted to know how to use Django with a src project layout, and also where I should put my settings for my Django project. It seemed like a bad idea to put settings in git but I couldn't find any recommendations or guides on exactly how to do what I wanted to do. I also didn't find any good information about how to use Django with uv, and especially Django projects and apps with uv, because the way uv handles dependencies is the right way but also very tricky!
So here goes. The way I've found that works very elegantly is something like this:
rm -rf /tmp/examples
mkdir -p /tmp/examples/project/src /tmp/examples/app_one/src /tmp/examples/app_two/src /tmp/examples/ww.app_three/src
cd /tmp/examples/project
uv init
uv add django gunicorn uvicorn uvicorn-worker
cd /tmp/examples/project
uv run django-admin startproject project src
cd /tmp/examples/
for f in $(find `pwd` -name \*app\* -type d); do cd $f && uv init && uv add django && cd -; done
find . -name hello.py -delete
mkdir -p app_one/src/
mkdir -p app_two/src/
mkdir -p ww.app_three/src/ww/
cd ww.app_three/src/ww/
uv run django-admin startapp app_three
for app in one two; do cd /tmp/examples/app_$app/src && uv run django-admin startapp app_$app; done
for app in one two; do cat <<EOF>>/tmp/examples/app_$app/src/app_$app/views.py
def simple(request):
return render(request, "app_$app/plain.html", {"app": "$app"})
EOF
done
for proj in $(find /tmp/examples -name pyproject.toml); do cat <<EOF>>$proj
[build-system]
requires = ["setuptools"]
EOF
done
cat <<EOF>/tmp/examples/project/pyproject.toml
[project]
name = "project"
version = "25.3.0b1"
description = "This stuff rocks!"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"django>=5.1.7",
"app_one",
"app_two",
"gunicorn",
"uvicorn",
"uvicorn-worker",
"ww.app_three",
]
[tool.uv.workspace]
members = [
"../app_one",
"../app_two",
"../ww.app_three",
]
[build-system]
requires = ["setuptools"]
[tool.uv.sources]
app-one = { workspace = true }
app-two = { workspace = true }
ww-app-three = { workspace = true }
EOF
cd /tmp/examples/project
uv sync && uv pip list
Now add "app_one.apps.AppOneConfig", "app_two.apps.AppTwoConfig", "ww.app_three.AppThreeConfig"
to INSTALLED_APPS
in
project/src/project/settings.py
.
In project/src/project/urls.py
(be sure to get from django.urls import include
):
urlpatterns = [
path("admin/", admin.site.urls),
path("one/", include('app_one.urls')),
path("two/", include('app_two.urls')),
path("three/",include('ww.app_three.urls')),
]
Now, app_three is going to have the wrong name so in
ww.app_three/src/ww/app_three/apps.py
you're going to need to change the name
to ww.app_three
.
Now you can go fast man! Run django using gunicorn for the reload and uvicorn for the asgi:
uv run gunicorn yourapp.asgi:application -k uvicorn_worker.UvicornWorker --reload
If you hit up http://localhost:8000 then, well... it won't work really, yet.
Cause you need some views. You can put them in
src/{app_name}/templates/{app_name}/whatever.html
and then in your views.py
you'd put
from django.shortcuts import render
def blerp(request):
return render(request, "app_name/whatever.html")
And then in your app.urls
you'll put
from django.urls import path
from . import views
urlpatterns = [
path("", views.blerp, name="something"),
]
And then you'll be able to check out the views, woohoo!
The one last thing -- settings? Well, as it turns out, Python has a
PYTHONPATH which will add those directories to the search/import path. And
Django has the DJANGO_SETTINGS_MODULE. Putting that all together you can
copy your settings to something like /etc/myproject/settings.py
or using the
above let's cp /tmp/examples/project/src/project/settings.py /tmp/fnord.py
and then we can run it like so:
PYTHONPATH=/tmp DJANGO_SETTINGS_MODULE=fnord uv run gunicorn yourapp.asgi:application -k uvicorn_worker.UvicornWorker --reload
And everything should work. Hope this helps!
~Wayne
ETX