Table of Contents
In the Django framework, a project refers to the collection of configuration files and code for a particular website. Django groups business logic into what it calls apps, which are the modules of the Django framework. There’s plenty of documentation on how to structure your projects and the apps within them, but when it comes time to package an installable Django app, information is harder to find.
In this tutorial, you’ll learn how to take an app out of a Django project and package it so that it’s installable. Once you’ve packaged your app, you can share it on PyPI so that others can fetch it through pip install
.
In this tutorial, you’ll learn:
- What the differences are between writing stand-alone apps and writing apps inside of projects
- How to create a
setup.cfg
file for publishing your Django app - How to bootstrap Django outside of a Django project so you can test your app
- How to test across multiple versions of Python and Django using
tox
- How to publish your installable Django app to PyPI using Twine
Even if you originally intend to make your Django app available as a package, you’re likely to start inside a project. To demonstrate the process of moving from Django project to installable Django app, I’ve made two branches available in the repo. The project branch is the starting state of an app inside of a Django project. The master branch is the finished installable app.
You can also download the finished app at the PyPI realpython-django-receipts package page. You can install the package by running pip install realpython-django-receipts
.
The sample app is a short representation of the line items on a receipt. In the project branch, you’ll find a directory named sample_project
that contains a working Django project. The directory looks like this:
sample_project/
│
├── receipts/
│ ├── fixtures/
│ │ └── receipts.json
│ │
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ │
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
│
├── sample_project/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
│
├── db.sqlite3
├── manage.py
├── resetdb.sh
└── runserver.sh
The most current version of Django at the time this tutorial was written was 3.0.4, and all testing was done with Python 3.7. None of the steps outlined in this tutorial should be incompatible with earlier versions of Django—I’ve used these techniques since Django 1.8. However, some changes are necessary if you’re using Python 2. To keep the examples simple, I’ve assumed Python 3.7 across the code base.
Creating the Django Project From Scratch
The sample project and receipts app were created using the Django admin
command and some small edits. To start, run the following code inside of a clean virtual environment:
$ python -m pip install Django
$ django-admin startproject sample_project
$ cd sample_project
$ ./manage.py startapp receipts
This creates the sample_project
project directory structure and a receipts
app subdirectory with template files that you’ll use to create your installable Django app.
sample_project/settings.py
file needs a few modifications:
- Add
'127.0.0.1'
to theALLOWED_HOSTS
setting so you can test locally. - Add
'receipts'
to theINSTALLED_APPS
list.
You’ll also need to register the receipts
app’s URLs in the sample_project/urls.py
file. To do so, add path('receipts/', include('receipts.urls'))
to the url_patterns
list.
Exploring the Receipts Sample App
The app consists of two ORM model classes: Item
and Receipt
. The Item
class contains database field declarations for a description and a cost. The cost is contained in a DecimalField
. Using floating-point numbers to represent money is dangerous—you should always use fixed-point numbers when dealing with currencies.
The Receipt
class is a collection point for Item
objects. This is achieved with a ForeignKey
on Item
that points to Receipt
. Receipt
also includes total()
for getting the total cost of Item
objects contained in the Receipt
:
# receipts/models.py
from decimal import Decimal
from django.db import models
class Receipt(models.Model):
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Receipt(id={self.id})"
def total(self) -> Decimal:
return sum(item.cost for item in self.item_set.all())
class Item(models.Model):
created = models.DateTimeField(auto_now_add=True)
description = models.TextField()
cost = models.DecimalField(max_digits=7, decimal_places=2)
receipt = models.ForeignKey(Receipt, on_delete=models.CASCADE)
def __str__(self):
return f"Item(id={self.id}, description={self.description}, " \
f"cost={self.cost})"
The model objects give you content for the database. A short Django view returns a JSON dictionary with all the Receipt
objects and their Item
objects in the database:
# receipts/views.py
from django.http import JsonResponse
from receipts.models import Receipt
def receipt_json(request):
results = {
"receipts":[],
}
for receipt in Receipt.objects.all():
line = [str(receipt), []]
for item in receipt.item_set.all():
line[1].append(str(item))
results["receipts"].append(line)
return JsonResponse(results)
The receipt_json()
view iterates over all the Receipt
objects, creating a pair of the Receipt
objects and a list of the Item
objects contained within. All of this is put in a dictionary and returned through Django’s JsonResponse()
.
To make the models available in the Django admin interface, you use an admin.py
file to register the models:
# receipts/admin.py
from django.contrib import admin
from receipts.models import Receipt, Item
@admin.register(Receipt)
class ReceiptAdmin(admin.ModelAdmin):
pass
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
pass
This code creates a Django ModelAdmin
for each of the Receipt
and Item
classes and registers them with the Django admin.
Finally, a urls.py
file registers a single view in the app against a URL:
# receipts/urls.py
from django.urls import path
from receipts import views
urlpatterns = [
path("receipt_json/", views.receipt_json),
]
You can now include receipts/urls.py
in your project’s url.py
file to make the receipt view available on your website.
With everything in place, you can run ./manage.py makemigrations receipts
, use the Django admin to add data, and then visit /receipts/receipt_json/
to view the results:
$ curl -sS http://127.0.0.1:8000/receipts/receipt_json/ | python3.8 -m json.tool
{
"receipts": [
[
"Receipt(id=1)",
[
"Item(id=1, description=wine, cost=15.25)",
"Item(id=2, description=pasta, cost=22.30)"
]
],
[
"Receipt(id=2)",
[
"Item(id=3, description=beer, cost=8.50)",
"Item(id=4, description=pizza, cost=12.80)"
]
]
]
}
In the block above, you use curl
to visit the receipt_json
view, resulting in a JSON response containing the Receipt
objects and their corresponding Item
objects.
Testing the App in the Project
Django augments the Python unittest
package with its own testing capabilities, enabling you to preload fixtures into the database and run your tests. The receipts app defines a tests.py
file and a fixture to test with. This test is by no means comprehensive, but it’s a good enough proof of concept:
# receipts/tests.py
from decimal import Decimal
from django.test import TestCase
from receipts.models import Receipt
class ReceiptTest(TestCase):
fixtures = ["receipts.json", ]
def test_receipt(self):
receipt = Receipt.objects.get(id=1)
total = receipt.total()
expected = Decimal("37.55")
self.assertEqual(expected, total)
The fixture creates two Receipt
objects and four corresponding Item
objects. Click on the collapsible section below for a closer look at the code for the fixture.
You can test the receipts app with the Django manage.py
command:
$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.013s
OK
Destroying test database for alias 'default'...
Running manage.py test
runs the single test defined in receipts/tests.py
and displays the results.
Making Your Installable Django App
Your goal is to share the receipts app without a project and to make it reusable by others. You could zip up the receipts/
directory and hand it out, but that’s somewhat limiting. Instead, you want to separate the app into a package so it’s installable.
The biggest challenge in creating an installable Django app is that Django expects a project. An app without a project is just a directory containing code. Without a project, Django doesn’t know how to do anything with your code, including running tests.
Moving Your Django App Out of the Project
It’s a good idea to keep a sample project around so you can run the Django dev server and play with a live version of your app. You won’t include this sample project in the app package, but it can still live in your repository. Following this idea, you can get started with packaging your installable Django app by moving it up a directory:
$ mv receipts ..
The directory structure now looks something like this:
django-receipts/
│
├── receipts/
│ ├── fixtures/
│ │ └── receipts.json
│ │
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ │
│ ├── __init__.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ ├── views.py
│ ├── admin.py
│ └── apps.py
│
├── sample_project/
│ ├── sample_project/
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ │
│ ├── db.sqlite3
│ ├── manage.py
│ ├── resetdb.sh
│ └── runserver.sh
│
├── LICENSE
└── README.rst
To package your app, you need to pull it out of the project. Moving it is the first step. I typically keep the original project around to test with, but I don’t include it in the resulting package.
Bootstrapping Django Outside of a Project
Now that your app is outside of a Django project, you need to tell Django how to find it. If you want to test your app, then run a Django shell that can find your app or run your migrations. You’ll need to configure Django and make it available.
Django’s settings.configure()
and django.setup()
are key to interacting with your app outside of a project. More information on these calls is available in the Django documentation.
You’re likely to need this configuration of Django in several places, so it makes sense to define it in a function. Create a file called boot_django.py
containing the following code:
1# boot_django.py
2#
3# This file sets up and configures Django. It's used by scripts that need to
4# execute as if running in a Django server.
5import os
6import django
7from django.conf import settings
8
9BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "receipts"))
10
11def boot_django():
12 settings.configure(
13 BASE_DIR=BASE_DIR,
14 DEBUG=True,
15 DATABASES={
16 "default":{
17 "ENGINE":"django.db.backends.sqlite3",
18 "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
19 }
20 },
21 INSTALLED_APPS=(
22 "receipts",
23 ),
24 TIME_ZONE="UTC",
25 USE_TZ=True,
26 )
27 django.setup()
Lines 12 and 27 set up the Django environment. The settings.configure()
call takes a list of arguments that are equivalent to the variables defined in a settings.py
file. Anything you would need in your settings.py
to make your app run gets passed into settings.configure()
.
The above code is a fairly stripped-down configuration. The receipts app doesn’t do anything with sessions or templates, so INSTALLED_APPS
only needs "receipts"
, and you can skip over any middleware definitions. The USE_TZ=True
value is necessary because the Receipt
model contains a created
timestamp. Otherwise, you would run into problems loading the test fixture.
Running Management Commands With Your Installable Django App
Now that you have boot_django.py
, you can run any Django management command with a very short script:
#!/usr/bin/env python
# makemigrations.py
from django.core.management import call_command
from boot_django import boot_django
boot_django()
call_command("makemigrations", "receipts")
Django allows you to programmatically call management commands through call_command()
. You can now run any management command by importing and calling boot_django()
followed by call_command()
.
Your app is now outside the project, allowing you to do all sorts of Django-y things to it. I often define four utility scripts:
load_tests.py
to test your appmakemigrations.py
to create migration filesmigrate.py
to perform table migrationsdjangoshell.py
to spawn a Django shell that’s aware of your app
Testing Your Installable Django App
The load_test.py
file could be as simple as the makemigrations.py
script, but then it would only be able to run all the tests at once. With a few additional lines, you can pass command-line arguments to the test runner, allowing you to run selective tests:
1#!/usr/bin/env python
2# load_tests.py
3import sys
4from unittest import TestSuite
5from boot_django import boot_django
6
7boot_django()
8
9default_labels = ["receipts.tests", ]
10
11def get_suite(labels=default_labels):
12 from django.test.runner import DiscoverRunner
13 runner = DiscoverRunner(verbosity=1)
14 failures = runner.run_tests(labels)
15 if failures:
16 sys.exit(failures)
17
18 # In case this is called from setuptools, return a test suite
19 return TestSuite()
20
21if __name__ == "__main__":
22 labels = default_labels
23 if len(sys.argv[1:]) > 0:
24 labels = sys.argv[1:]
25
26 get_suite(labels)
Django’s DiscoverRunner
is a test discovery class compatible with Python’s unittest
. It’s responsible for setting up the test environment, building the suite of tests, setting up the databases, running the tests, and then tearing it all down. Starting on line 11, get_suite()
takes a list of test labels and directly calls the DiscoverRunner
on them.
This script is similar to what the Django management command test
does. The __main__
block passes any command-line arguments to get_suite()
, and if there are none, then it passes in the test suite for the app, receipts.tests
. You can now call load_tests.py
with a test label argument and run a single test.
Line 19 is a special case to help when testing with tox
. You’ll learn more about tox
in a later section. You can also check out a potential substitute for DiscoverRunner
in the collapsible section below.