Django Classy Settings¶
Stay classy, Django.
Credits¶
This work was originally inspired by the work of Jessie O’Connor.
Overview¶
Class-based settings make it easy for you to manage multiple settings profiles for your Django project, without ever copying values.
Interdependant values, values sourced from the env, even calculated values are no problem, since you have the full power of Python and class inheritance.
Contents¶
Examples¶
Below is a default Django settings.py that has been adjusted to show how you might use django-classy-settings:
1 """
2 Django settings for dummy project.
3
4 Generated by 'django-admin startproject' using Django 3.2.15.
5
6 For more information on this file, see
7 https://docs.djangoproject.com/en/3.2/topics/settings/
8
9 For the full list of settings and their values, see
10 https://docs.djangoproject.com/en/3.2/ref/settings/
11 """
12
13 from pathlib import Path
14
15 from cbs import BaseSettings, env
16
17 # Build paths inside the project like this: BASE_DIR / 'subdir'.
18 BASE_DIR = Path(__file__).resolve().parent.parent
19
20
21 # Quick-start development settings - unsuitable for production
22 # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
23
24 ALLOWED_HOSTS = []
25
26 ROOT_URLCONF = 'dummy.urls'
27
28 TEMPLATES = [
29 {
30 'BACKEND': 'django.template.backends.django.DjangoTemplates',
31 'DIRS': [],
32 'APP_DIRS': True,
33 'OPTIONS': {
34 'context_processors': [
35 'django.template.context_processors.debug',
36 'django.template.context_processors.request',
37 'django.contrib.auth.context_processors.auth',
38 'django.contrib.messages.context_processors.messages',
39 ],
40 },
41 },
42 ]
43
44 WSGI_APPLICATION = 'dummy.wsgi.application'
45
46 # Password validation
47 # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
48
49 AUTH_PASSWORD_VALIDATORS = [
50 {
51 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
52 },
53 {
54 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
55 },
56 {
57 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
58 },
59 {
60 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
61 },
62 ]
63
64 # Internationalization
65 # https://docs.djangoproject.com/en/3.2/topics/i18n/
66
67 LANGUAGE_CODE = 'en-us'
68
69 TIME_ZONE = 'UTC'
70
71 USE_I18N = True
72
73 USE_L10N = True
74
75 USE_TZ = True
76
77
78 # Static files (CSS, JavaScript, Images)
79 # https://docs.djangoproject.com/en/3.2/howto/static-files/
80
81 STATIC_URL = '/static/'
82
83 # Default primary key field type
84 # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
85
86 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
87
88
89 class Settings(BaseSettings):
90
91 # Allow production to override the secret key, but fall-back to something consistent.
92 SECRET_KEY = env('django-insecure-kcdhlitokdqc7s7p7)^jm)55%p@frm#l39nzko458#1!6cu*$$')
93
94 # DEBUG defaults to True, but can be overridden by env var `DJANGO_DEBUG`
95 DEBUG = env.bool(True, prefix='DJANGO_')
96
97 # Simple cases that don't need `self` can even use a lambda
98 MEDIA_ROOT = env(lambda self: BASE_DIR / 'media')
99
100 # Methods will be transparently invoked by the __getattr__ implementation
101 def INSTALLED_APPS(self):
102 return list(filter(None, [
103 'django.contrib.admin',
104 'django.contrib.auth',
105 'django.contrib.contenttypes',
106 'django.contrib.sessions',
107 'django.contrib.messages',
108 'django.contrib.staticfiles',
109 # Conditionally include an app
110 'debug_toolbar' if self.DEBUG else None,
111 ]))
112
113 def MIDDLEWARE(self):
114 return list(filter(None, [
115 'django.middleware.security.SecurityMiddleware',
116 'django.contrib.sessions.middleware.SessionMiddleware',
117 'django.middleware.common.CommonMiddleware',
118 'django.middleware.csrf.CsrfViewMiddleware',
119 'django.contrib.auth.middleware.AuthenticationMiddleware',
120 'django.contrib.messages.middleware.MessageMiddleware',
121 'django.middleware.clickjacking.XFrameOptionsMiddleware',
122 # Conditionally include a middleware
123 'debug_toolbar.middleware.DebugToolbarMiddleware' if self.DEBUG else False,
124 ]))
125
126 # Parse the URL into a database config dict.
127 DEFAULT_DATABASE = env.dburl('sqlite:///db.sqlite')
128
129 def DATABASES(self)
130 return {
131 'default': self.DEFAULT_DATABASE,
132 }
133
134
135 class ProdSettings(Settings):
136
137 # Override
138 DEBUG = False
139
140 # Values that *must* be provided in the environment.
141 @env
142 def STATIC_ROOT(self):
143 raise ValueError('STATIC_ROOT not supplied!')
144
145 # The `use` method will find the right sub-class of ``BaseSettings`` to use
146 # Based on the value of the `DJANGO_MODE` env var.
147 __getattr__, __dir__ = Settings.use()
Now when you start Django, it will use all of your global settings, and any
from Settings
.
You can switch to using the ProdSettings
by setting the DJANGO_MODE
environment variable:
# Use default Settings
$ ./manage.py shell
# Use ProdSettings
$ DJANGO_MODE=prod ./manage.py shell
BaseSettings.use()
picks the BaseSettings
sub-class named
{DJANGO_MODE.title()}Settings
.
Note
Since the registry of subclasses is on BaseSettings
, you can call
.use()
on any sub-class and it will behave the same.
Which settings to move?¶
Generally, only move settings which are either environment driven, or need per-mode control.
Because of the precedence rules of Python’s module level __getattr__
function, any settings declared outside a class can not be overridden by a
class-based setting.
GLOBAL = "global"
class Settings(BaseSettings):
GLOBAL = "local" # This setting will never be used
The env property¶
To help with environment driven settings there is the env property decorator.
The simplest use case is with an immediate value:
class Settings(BaseSettings):
FOO = env('default')
__getattr__, __dir__ = BaseSettings.use()
In this case, if the FOO environment variable is set, then settings.FOO
will yield its value. Otherwise, it will be "default"
.
You can optionally override the environment variable name to look up by passing
a key
argument:
class Settings(BaseSettings):
FOO = env('default', key='BAR')
Additionally, you can define a prefix for the environment variable:
class Settings(BaseSettings):
FOO = env('default', prefix='MY_') # looks up env var MY_FOO
If you need a type other than str
, you can pass a cast
callable, which
will be passed the value.
class Settings(BaseSettings):
FOO = env('default', cast=int)
Pre-defined cast types¶
For convenience, there are several built-in pre-defined cast types, accessible
via the env
decorator.
env.bool # Treats ("y", "yes", "on", "t", "true", "1") as True, and ("n", "no", "off", "f", "false", "0") as False
env.int # Use the int constructor
env.dburl # Converts URLs to Django DATABASES entries.
env.list # splits on ',', and strips each value
env.tuple # as above, but yields a tuple
In all cases, if the default value passed is a string, it will be passed to the cast function.
As a decorator¶
Additionally, env
will function as a decorator. This allows you to put some
logic into deciding the default value.
class Settings(BaseSettings):
DEBUG = True
@env.int
def FOO(self):
return 1 if self.DEBUG else 2
Mandatory environment variable¶
Should you require a value to always be supplied by environment variable,
have your method raise a ValueError
class Settings(BaseSettings):
@env.int
def REQUIRED(self):
raise ValueError()
Avoiding repeated prefixes¶
To avoid having to specify a prefix on multiple related variables, env
will
yield a partial
when no default is provided.
Let’s say, for instance, you want all of your environment variables prefixed with DJANGO_
# Common prefix for DJANGO related settings
denv = env['DJANGO_']
class Settings(BaseSettings):
DEBUG = denv(True) # Will look for DJANGO_DEBUG in env
Now setting DJANGO_DEBUG=f
will disable debug mode.