Django single file project

Explore and find ways to bend it until it breaks.

Disclaimer?

The main purpose of this post is learning. Like any tool we should explore and find ways to bend it until it breaks. You should normally not use those techniques in real life projects right? For this post, I'm assuming you're using Django 1.10.4, and Python3.6.

Single file?

Before reading Lightweight Django, I never tought it was possible to build a Django project in a single python file. For that kind of stuff, I usually think, Flask, or Bottle. And in fact, there are many others.

After reading the first chapter, I was a little bit disappointed. There were no Django models involved in the shown example. So, I try to find out if it was possible: a single file Django project with at least one application containing one model. I found some interesting links on the topic. But either it was for a very old version of Django, either I was obliged to install another package besides Django. After few days reading others people code, this is what I've done:

#!/usr/bin/env python

import os
import sys
from django.conf import settings

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

sys.path[0] = os.path.dirname(BASE_DIR)

# the current folder name will also be our app
APP_LABEL = os.path.basename(BASE_DIR)


settings.configure(
    DEBUG=os.environ.get('DEBUG', 'on') == 'on',
    SECRET_KEY=os.environ.get('SECRET_KEY', os.urandom(32)),
    ALLOWED_HOSTS=os.environ.get('ALLOWED_HOSTS', 'localhost').split(','),
    ROOT_URLCONF=__name__,
    MIDDLEWARE=[
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
        'django.middleware.locale.LocaleMiddleware',
        ],
    INSTALLED_APPS=[
        APP_LABEL,
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'rest_framework',
        ],
    STATIC_URL='/static/',
    STATICFILES_DIRS=[
        os.path.join(BASE_DIR, "static"),
    ],
    STATIC_ROOT=os.path.join(BASE_DIR, "static_root"),
    MEDIA_ROOT=os.path.join(BASE_DIR, "media"),
    MEDIA_URL='/media/',
    TEMPLATES=[
        {
            'BACKEND': 'django.template.backends.django.DjangoTemplates',
            'DIRS': [os.path.join(BASE_DIR, "templates"),],
            'APP_DIRS': True,
            'OPTIONS': {
                'context_processors': [
                    'django.template.context_processors.debug',
                    'django.template.context_processors.i18n',
                    'django.template.context_processors.request',
                    'django.contrib.auth.context_processors.auth',
                    'django.template.context_processors.tz',
                    'django.contrib.messages.context_processors.messages',
                ],
            },
        },
        ],
    DATABASES={
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
            }
        },
    REST_FRAMEWORK={
        'DEFAULT_PERMISSION_CLASSES': [
            'rest_framework.permissions.IsAdminUser',
        ],
        'PAGE_SIZE': 10
    }
)


import django
django.setup()


from django.db import models
from django.contrib import admin

from django.db import models

# Create your models here.

class Author(models.Model):
    name = models.CharField(max_length=200)
    class Meta:
        app_label = APP_LABEL

class Book(models.Model):
    author = models.ForeignKey(Author, related_name='books')
    title = models.CharField(max_length=400)
    class Meta:
        app_label = APP_LABEL

admin.site.register(Book)
admin.site.register(Author)
admin.autodiscover()


from rest_framework import serializers

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

from rest_framework import viewsets

class BooksViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer



from django.conf.urls import url, include
from rest_framework import routers
from django.http import HttpResponse
from django.contrib import admin

router = routers.DefaultRouter()
router.register(r'books', BooksViewSet)

def index(request):
    return HttpResponse("Hello")


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', index, name='homepage'),
    url(r'^api/', include(router.urls)),
    url(r'^api-auth/', include('rest_framework.urls',\
                                   namespace='rest_framework'))
]


from django.core.wsgi import get_wsgi_application

if __name__ == "__main__":
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)
else:
    get_wsgi_application()

Everything related to Django REST Framework is not mandatory. I was looking for interesting links over the internet, and I found this question on StackOverflow. I saw this as an opportunity to kill two birds with one stone.

The above script was saved in a file named single.py in a folder named lwdj. But you can do as you feel, as soon as you respect the name of the file and the name of the current folder. The rest is known story:

Make the migrations for our application:

(dj) ~/.../how_to_django/lwdj  (master *%)
⤷ python single.py makemigrations lwdj
Migrations for 'lwdj':
  migrations/0001_initial.py:
    - Create model Author
    - Create model Book
(dj) ~/.../how_to_django/lwdj  (master *%) ⤷

Run the migrations:

(dj) ~/.../how_to_django/lwdj  (master *%)
⤷ python single.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, lwdj, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying lwdj.0001_initial... OK
  Applying sessions.0001_initial... OK
(dj) ~/.../how_to_django/lwdj  (master *%)

Create a super user and run the server:

(dj) ~/.../how_to_django/lwdj  (master *%)
⤷ python single.py createsuperuser
Username (leave blank to use 'nsukami'):
Email address: nsukami@gmail.com
Password:
Password (again):
Superuser created successfully.
(dj) ~/.../how_to_django/lwdj  (master *%)
⤷ python single.py runserver
Performing system checks...

System check identified no issues (0 silenced).
January 12, 2017 - 18:52:57
Django version 1.10.4, using settings None
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.



So, what have we learned here?

Yes, that is not how you are supposed to use Django. But we were able to learn few things.

Projects and applications

Somewhere in the Django documentation related to projects and applications, we can read:

There's no restriction that a project package can't also be considered an application and have models, etc. (which would require adding it to INSTALLED_APPS).

We can double check using the tree command:

(dj) ~/.../how_to_django/lwdj  (master *%)
⤷ tree
.
├── activate -> /home/nsukami/envs/dj/bin/activate
├── db.sqlite3
├── Makefile
├── media
├── migrations
│   ├── 0001_initial.py
│   ├── __init__.py
│   └── __pycache__
│       └── __init__.cpython-36.pyc
├── single.py
├── static
│   └── site.css
├── static_root
└── templates
    └── home.html

The django.setup() method

Reading The Fabulous Manual, we learn that, this method configures Django by loading the settings, but also:

When Django starts, django.setup() is responsible for populating the application registry.

Let's start a shell using the command python single.py shell, and let's check the application registry:

(dj) >>> from django.apps import apps
(dj) >>> apps.ready
True
(dj) >>>
(dj) >>>
(dj) >>> apps.get_app_configs()
odict_values([<AppConfig: lwdj>, <AdminConfig: admin>, <AuthConfig: auth>, <ContentTypesConfig: contenttypes>, <SessionsConfig: sessions>, <MessagesConfig: messages>, <StaticFilesConfig: staticfiles>, <AppConfig: rest_framework>])
(dj) >>>

(dj) >>> c = apps.get_app_config('lwdj')
(dj) >>>
(dj) >>> c.module
<module 'lwdj' (namespace)>
(dj) >>>
(dj) >>> c.path
'/home/nsukami/GITHUB/how_tos/how_to_django/lwdj'
(dj) >>>
(dj) >>> c.verbose_name
'Lwdj'
(dj) >>>
(dj) >>> c.name
'lwdj'
(dj) >>> c.label
'lwdj'
(dj) >>>
(dj) >>> for m in c.models: print(m)
...
author
book
(dj) >>>

Neat!

We also read that:

At this stage, your code shouldn't import any models!

This is exactly why our models, and everything related to them, do not appear before those 2 lines of code.

import django
django.setup()

Doing otherwise, raises the AppRegistryNotReady exception:

(dj) ~/.../how_to_django/lwdj  (master *%)
⤷ python single.py runserver
Traceback (most recent call last):
  File "single.py", line 90, in <module>
    class Author(models.Model):
  File "/home/nsukami/envs/dj/lib/python3.6/site-packages/django/db/models/base.py", line 105, in __new__
    app_config = apps.get_containing_app_config(module)
  File "/home/nsukami/envs/dj/lib/python3.6/site-packages/django/apps/registry.py", line 237, in get_containing_app_config
    self.check_apps_ready()
  File "/home/nsukami/envs/dj/lib/python3.6/site-packages/django/apps/registry.py", line 124, in check_apps_ready
    raise AppRegistryNotReady("Apps aren't loaded yet.")
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
(dj) ~/.../how_to_django/lwdj  (master *%)

Installed applications

I have not yet fully understood this part. APP_LABEL is actually containing the name of the current directory. By putting APP_LABEL inside INSTALLED_APPS, Django was able to find all the Django models existing inside the current directory:

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path[0] = os.path.dirname(BASE_DIR)
APP_LABEL = os.path.basename(BASE_DIR)
{{< /highlight >}}
{{< highlight python >}}
    INSTALLED_APPS=[
        APP_LABEL,
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'rest_framework',
        ],

App label

All our models were created outside of a models.py file. They're not even in a real Django application. So we were obliged to tell them which application they belongs to. This is done using the app_label attribute of the internal class Meta of Django models. {{< highlight python >}} class Author(models.Model): name = models.CharField(max_length=200) class Meta: app_label = APP_LABEL

class Book(models.Model): author = models.ForeignKey(Author, related_name='books') title = models.CharField(max_length=400) class Meta: app_label = APP_LABEL {{< /highlight >}}

Really hope you learnt something. Also, I would not be able to write this post, without following the StackTrace leaved by the people who were there before me. A huge thanks to them: