Home | Blag Index


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.

Django Projects, Apps, Config, and You

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


Home | Blag Index

This site is Copyleft Wayne Werner - BY-NC-SA 4.0