toxinu

Pip installable Django project

Posted on 16 December 2016

I’ll present you a Django layout which allow to ship your project (not app) as a Python package. Your users will be able to install it with pip for example.

This template is available as a Cookiecutter template. But I’ll remove every references, because this paper is mainly written to present you a Django project layout. Maybe I’ll do another post about Cookiecutter later.

My first need is to be able to install my Django project via pip install my-awesome-app and access it via my-awesome-app entry point without losing manage.py flexibility. The first reference for this template is Sentry project. I’m really appreciative of how this project is nicely built by nice people.

Let’s describe how it works!

Layout

This is a little description of my typical layout for every new Django project I build.

 1 $ tree .
 2 .
 3 ├── conftest.py
 4 ├── Makefile
 5 ├── MANIFEST.in
 6 ├── README.rst
 7 ├── setup.cfg
 8 ├── setup.py
 9 ├── src
10 │   └── my_awesome_project
11 │       ├── __about__.py
12 │       ├── apps
13 │       │   ├── app_one
14 │       │   └── app_two
15 │       ├── core
16 │       │   ├── apps.py
17 │       │   ├── views.py
18 │       │   └── [...]
19 │       ├── runner
20 │       │   ├── commands
21 │       │   │   ├── django.py
22 │       │   │   └── help.py
23 │       │   └── decorators.py
24 │       ├── settings
25 │       │   ├── base.py
26 │       │   └── local.dist.py
27 │       ├── urls.py
28 │       └── wsgi.py
29 └── tests
30     └── core
31         └── test_core.py

In order to have a pip installable project we need to have a setup.py like every Python package. You’ll notive that we don’t need requirements.txt at all. Hooray! All your dependencies will be organized in setup.py. No more requirements-[dev|test].txt files, setuptools extras_require option will do the trick.

You’ll probably need a core app (line 15) and many other apps (line 12). Another special directory is runner where your command line entry points will be stored.

Django community have various ways to organize settings, feel free to use your own but I prefer to have a base.py (line 25) settings file and another local.py which will be imported in settings/__init__.py.

It’s always a good thing to give users local.dist.py (line 26) or production.dist.py example files, as long as Django settings number always tend to grow. Always try to put a little description below every settings that can be complex. But don’t try to describe all them all if it seems easy to understand. This kind of documentation can be difficult to keep up-to-date.

LOGIN_URL = 'https://localhost:8000/login'
# Set these to drop to an unprivileged user after
# binding the SMTP daemon (Default: False)
DROP_PRIVILEGES_USER = False
# Product name appear in email templates and optouts pages
PRODUCT_NAME = 'My Awesome App'

Install process

A developper install process will be something like that:

A production deployment for a PyPI published project will be:

Let’s take a quick look at setup.py:

[...]
	entry_points={
        'console_scripts': [
            'my-awesome-project = my_awesome_project.runner:main']
    },
    extras_require={
        'dev': [
            'bumpversion==0.5.3',
            'docker-compose==1.9.0'
        ],
        'tests': [
            'pytest==3.0.5',
            'flake8==3.2.1',
            'libfaketime==0.4.2',
            'factory-boy==2.7.0',
            'fake-factory==0.7.2'
        ],
    },
[...]

This extra_require option is pretty useful. You can have a production one which contains for example Gunicorn or Psycopg, etc…

Where is my manage.py?

First of all, don’t panic! You still have django-admin command line tool which is very similar to manage.py.

Instead of python manage.py shell, you can do:

DJANGO_SETTINGS_MODULE=my_awesome_project.settings django-admin shell

Ok, it’s very verbose. This is why this template give you another approach with runner which use click library.

Main goal is to be able to do something like:

my-awesome-project django shell

Then every Django commands will be accessible. You can take a look at how it works here.

This is a dead simple example of a custom command which return Django version.

$ cat src/my_awesome_project/runner/commands/version.py
import click
import django


@click.command()
def version():
    "Show Django version."
    click.echo('Django {}'.format(django.__version__))

Then you’ll be able to do:

$ my-awesome-project version
Django 1.10.1

End

I know every Django or project layouts can be personnal but here, I want to show you how to ship a Django project in a professional way.

You can retrieve this template here.

Feel free to contact me for any questions, leave issue on Github.

In further posts I’ll speak about: “How to store package metadata”, “How to trash your unmaintainable fixtures” and more tools like bumpversion and docker-compose.