App settings
Every Django project is configurable by a settings.py
file (or othwerise
named and specified settings file). This file provides for the configuration
needs of a site that may differ from deployment to deployment - like desktop
development and production - and from day to day.
Individual apps often have their own configuration needs. This can involve toggling certain features, API credentials, cache key and timeout information - there's a wide range of knobs which may warrant tuning depending on what the end user or deployed site needs.
Sourcing app settings
There are a few obvious candidates for how to source your app's specific settings. We'll review in order of least attractive to most attractive.
The environment
If you're following the 12 factor application pattern you likely have settings for your site that are provided via environment variables. This is a fine way of providing project level configuration, but a poor one for providing direct configuration to sub-apps.
Let's say you have a Django app that includes a third-party API client. In your own
project you provide the API credentials through the process environment, so you
simply pull these from the environ
in your API client module like so:
# myapp/client.py
import os
API_TOKEN = os.environ.get("API_TOKEN")
class ApiClient:
"""All client code follows from here"""
...
This certainly works but suffers from some serious problems.
First and sufficiently it necessitates that every project using your app provides settings via the process environment. This is hardly the only way to specify unique settings and it's an unnecessary imposition for your package to dictate how the projects using it provide their settings.
For people who are providing individual setting values via environment variables it dictates what those variables must be named which in most cases is not something your package should have any reason to dictate. Whether or not the names you have chosen are descriptive and unique, other people may have very good reasons to use variables with other names.
And for non-secret settings, it may make more sense for these to be hard coded on a per-deployment basis (e.g. for a production settings module vs a development settings module). In this case your environment variable-based settings add non-trvial overhead.
Avoid this pattern in your Django app!
Directly from django.conf.settings
Far better is to delegate how these settings are provided to the project's own settings module.
# myapp/client.py
from django.conf import settings
API_TOKEN = settings.API_TOKEN
class ApiClient:
"""All client code follows from here"""
...
This solves the problem of how the individual settings values are added to the project. However as is it doesn't do anything to provide default values or any kind of setting validation.
App configuration modules
We can actually do one better than our previous example of importing directly from the project settings by instead using an intermediary app-specific settings module.
To motivate this, let's look at an example where we want a default value. An API credential is a bad example, but something like a "retry" count could have a default value.
# myapp/client.py
from django.conf import settings
try:
API_RETRIES = settings.API_RETRIES
except AttributeError:
API_RETRIES = 0
class ApiClient:
"""All client code follows from here"""
...
This code ensures there is a value available whether one is specified or not. We probably want to go further than this though and provide at least some minimal validation. The problem with settings values is that when they're wrong because the value is wrong (e.g. wrong type) it's not often immediately clear why, and instead the user is presented with a stacktrace that might extend to the other libraries. So if there are hard limits to a setting try to enforce them upfront.
# myapp/client.py
from django.conf import settings
from django.core.excpetions import ImproperlyConfigured
try:
API_RETRIES = int(settings.API_RETRIES)
except AttributeError:
API_RETRIES = 0
except(TypeError, ValueError):
raise ImproperlyConfigured("API_RETRIES must be an integer value")
if not 0 <= API_RETRIES <= 10:
raise ImproperlyConfigured("API_RETRIES must be between 0 and 10")
class ApiClient:
"""All client code follows from here"""
...
Now, not every setting demands this kind of validation! But you can see how this can quickly create clutter, not to mention duplication in the event a value is needed in more than one place in your app.
Instead, use a separate module in your app from which you can cleanly import the necessary settings, validated and with sane defaults.
# myapp/conf.py
from django.conf import settings
from django.core.excpetions import ImproperlyConfigured
try:
API_RETRIES = int(settings.API_RETRIES)
except AttributeError:
API_RETRIES = 0
except(TypeError, ValueError):
raise ImproperlyConfigured("API_RETRIES must be an integer value")
if not 0 <= API_RETRIES <= 10:
raise ImproperlyConfigured("API_RETRIES must be between 0 and 10")
# myapp/client.py
from myapp.conf import API_RETRIES
class ApiClient:
"""All client code follows from here"""
...
Settings formats
There are two basic ways of collecting application settings from a
project level settings.py
file:
- Individually named settings
- Monolithic dictionaries
In either case, the settings values your app looks for in
django.conf.settings
should be consistently namespaced to prevent
collisions with other app settings and to make the settings easily
identifiable.
SOMETHING_COOL = True
That's not helpful.
MYAPP_SOMETHING_COOL = True
Now this immediately describes what this setting is associated with.
Individual named settings
This should be your default choice, individual values in the settings files, namespaced by your choice of app settings namespace.
MYAPP_SOMETHING_COOL = True
MYAPP_SAY_HELLO = 'whatever.models.Okay'
MYAPP_IS_IT_ON = DEBUG
First, most apps have only a few settings. This pattern follows conventions
and is easy to work with. It can get a bit ungainly if there are many settings
values though, or any kind of nested settings (Django's TEMPLATES
setting
should come to mind).
Monolithic dictionary
This doesn't have to be a dictionary per se, e.g. a tuple would work as well, but a dictionary makes the most sense here.
CUMULUS = {
'USERNAME': 'YourUsername',
'API_KEY': 'YourAPIKey',
'CONTAINER': 'ContainerName',
'PYRAX_IDENTITY_TYPE': 'rackspace',
}
This style of setting makes sense when you have required configuration, like to a backing service. If the alternative is something like this in every single settings.py file:
MYAPP_USERNAME = 'YourUsername',
MYAPP_API_KEY = 'YourAPIKey',
MYAPP_CONTAINER = 'ContainerName',
MYAPP_PYRAX_IDENTITY_TYPE = 'rackspace',
Then a consolidated dictionary will be easier to read and understand, for both you as the third-part developer and whoever is using your app. The specifics of how these values are populated are up to the end user, and in any case do not differ substantially.
The primary drawback to using a nested structure like a dictionary is that it is not easily overrided in part. If a number of non-secret settings are used across a Django site with different deployment configurations, then either the entire setting dictionary must be defined again or the original must be mutated in place.
Quick example:
# myapp/settings/staging.py
from myapp.settings import base
base.MYAPP_API['RETRIES'] = 4
The lesson here is to at least have a good reason for using a dictionry or other container for app settings and to ensure they are settings that you would expect to be updated in tandem.
Mixing settings styles
The two styles of settings are not mutually exclusive.
MYAPP_API = {
'USERNAME': 'YourUsername',
'API_KEY': 'YourAPIKey',
'CONTAINER': 'ContainerName',
'PYRAX_IDENTITY_TYPE': 'rackspace',
}
MYAPP_USE_CACHING = False