Don’t import (too much) in your django settings

Tags: django

One of our production Django sites broke this afternoon with a database error “relation xyz doesn’t exist”. So: a missing table.

Why 1

I helped debugging it and eventually found the cause by doing a select * from south_migrationhistory. This lists the south migrations and lo and behold, a migration had just been applied 25 minutes earlier. The migration name suggested a rename of tables, which of course matches the “missing table” error.

Why 2

Cause found. But you have to ask yourself “why” again. So: “why was this migration applied?”.

Well, someone was working on a bit of database cleanup and refactoring. Naming consistency, proper use of permissions, that sort of thing. Of course, locally in a branch. And on a development database. Now why did the local command result in a migration on the production database?

Why 3

So, effectively, “why don’t the development settings work as intended”? We normally use settings.py as the production settings and a developmentsettings.py that is used in development. It imports from settings.py and sets the debug mode and development database and so.

This project is a bit different in that there’s only a settings.py. It does however try to import localsettings.py. This is generated for you when you set up your project environment with ansible. A bit less clear (in my opinion) than a real .py file in your github repository, but it works. We saw the generated localsettings file with development database and DEBUG = True. This wasn’t the cause. What then?

Normally, calling django’s diffsettings command (see the django documentation) shows you any settings errors by printing all the settings in your config that are different from Django’s defaults. In this case, nothing was wrong. The DATABASES setting was the right one with the local development database. Huh?

The developer mentioned one other thing he changed recently: importing some django signal registration module in the ``settings.py``. Ah! Django’s signals often work on the database. Yes, the signals in this module did database work, too.

So the settings.py effectively looked like this:

DATABASES = { .... 'server': 'productiondatabase' ....}
import my_project.signal_stuff_that_works_on_the_database
try:
    from .localsettings import *
    # This normally sets DATABASES = { .... 'server': 'developmentdatabase' ....}
except ImportError:
    pass

The import of the signal registration module apparently triggered something in Django’s database layer so that the database connection was already active. The subsequent change of the DATABASES config to the local development database didn’t have any effect anymore.

diffsettings just shows you what the settings are and doesn’t catch the fact that the DATABASES isn’t really used in the form that comes out of diffsettings.

Why 4

Why the import, then?

Well, it has to be executed when django starts up. The settings file looked like a good spot. It isn’t, though.

The traditional location to place imports like this is the urls.py or models.py file. That’s why the admin.autodiscover() line is often in your urls.py, for instance.

So... put imports like this in models.py or urls.py instead of in your settings file.

Why 5

Digging even deeper... isn’t this sort of weird and ugly? Why isn’t there a more obvious place for initialization code like this? Now you have to have the arcane knowledge to somehow know where you can import and where not, right?

The answer: there is a good spot, in django 1.7. The AppConfig.ready() method! Quote from the documentation: Subclasses can override this method to perform initialization tasks such as registering signals. Bingo!

blog comments powered by Disqus
 
vanrees.org logo

About me

My name is Reinout van Rees and I work a lot with Python (programming language) and Django (website framework). I live in The Netherlands and I'm happily married to Annie van Rees-Kooiman.

Weblog feeds

Most of my website content is in my weblog. You can keep up to date by subscribing to the automatic feeds (for instance with Google reader):