'The Other True Way'
A case for Class Based Settings
David Seddon
david@seddonym.me
http://seddonym.me
Writing settings is boring.
Let's do less of it!
The Settings Holy Grail
The Settings Holy Grail
-
Easy deployment: project-level config stored in codebase
-
Security: sensitive settings stored locally
- Maintainability: small settings files
1. 'The One True Way'
2. The problem with that way
3. An alternative
1. 'The One True Way'
Jacob Kaplan-Moss
Benevolent Dictator for Life, Django
"Do the simplest thing that
could possibly work"
settings
- __init__.py
- base.py
- local.py
- staging.py
- production.py
# base.py
INSTALLED_APPS = [
'myapp',
]
# local.py
from .base import *
INSTALLED_APPS += [
'debug_toolbar'
]
The One True Way
2. The problem with
The One True Way
What's the problem?
Difficult to stay DRY
It's difficult to stay DRY, because it's difficult to write settings that depend on other settings
Settings depending on other settings
- an example
ALLOWED_HOSTS = ['mysite.com']
BASE_URL = 'http://mysite.com'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'mysite',
'USER': 'mysite',
'PASSWORD': '',
}
}
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'null': {
'level': 'DEBUG',
'class': 'logging.NullHandler',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
}
},
'loggers': {
'django': {
'handlers': ['null'],
'propagate': True,
'level': 'INFO',
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
},
'mysite.custom': {
'handlers': ['console', 'mail_admins'],
'level': 'INFO',
}
}
}
# local.py
from .base import *
SITENAME = 'mysite'
# What do we do here?
from .secret import *
How do we simplify it?
Using the One True Way results in long settings files.
3. An alternative
Bobsleigh
https://github.com/seddonym/bobsleigh
Bobsleigh file structure
myproject
__init__.py
manage.py
wsgi.py
settings
__init__.py
project.py
installations.py
secret.py
Bobsleigh's runners
# manage.py
#!/usr/bin/env python
if __name__ == "__main__":
from bobsleigh.runner import manage_environment
manage_environment()
# wsgi.py
from bobsleigh.runner import wsgi_environment
application = wsgi_environment()
project.py
The basic settings for the project.
from django.conf.global_settings import *
INSTALLED_APPS += [
'myapp',
]
secret.py
Sensitive settings not under version control.
# secret.py
SECRET_KEY = 'w&P%jcQM&cz5%Nmdkn$wyKI8Mx8G3ZZ!S^kUX^5mKSom7t5pqLKtLGV!EOn$Y7^K6ih470Hz'
DB_PASS = 'mypassword'
installations.py
Describes how settings should be managed for each installation. Class based settings!
from bobsleigh.handlers import InstallationHandler
INSTALLATIONS = [
# Local installation
InstallationHandler(domain='mysite.localhost',
host='localhost'),
# Dev installation
InstallationHandler(domain='dev.mysite.com',
host='mydevserver'),
# Live installation
InstallationHandler(domain='mysite.com',
host='myliveserver'),
]
# Local installation
InstallationHandler(domain='mysite.localhost',
host='localhost')
InstallationHandlers in more detail
Simplistic example
InstallationHandlers in more detail
InstallationHandler(
domain='mysite.localhost',
host='localhost',
debug=True,
monitor=True
virtualenv_path='/opt/.virtualenvs/mysite',
log_path='/var/log/mysite',
project_path='/opt/www/mysite',
static_path='/opt/var/www/mysite/static',
media_path='/opt/var/www/mysite/media',
db_name='mysite',
db_user='mysite',
python='python3.0',
server_email='Mysite <contact@mysite.localhost>',
email_host_user='mysite',
email_host='smtp.mailserver.com',
extra_settings={
'CELERY_ALWAYS_EAGER': True,
}
)
Real world example
To stay DRY, extend InstallationHandlers!
InstallationHandlers in more detail
class LocalHandler(InstallationHandler):
"For my local development setup."
# Has an config object self.config, with various attributes
def get_required_kwargs(self):
# Returns required kwargs, which will be set on self.config
def get_optional_kwargs(self):
# Returns optional kwargs, with defaults, to be set on self.config
def get_config_patterns(self):
# Specifies how to set self.config attributes based on the values
# of other self.config attributes
def adjust(self):
# Uses the self.config attributes to build the settings object
# that will eventually be available as django.conf.settings
InstallationHandlers in more detail
class LocalHandler(InstallationHandler):
"For my local development setup."
def get_required_kwargs(self):
return ('sitename',)
def get_optional_kwargs(self):
optional_kwargs = super(LocalHandler, self).get_optional_kwargs()
optional_kwargs.update({
'host': 'lanky',
'debug': True,
'monitor': True,
'email_host': 'smtp.gmail.com',
'email_use_tls': True,
'email_port': 587,
'email_host_user': 'davidseddonis@gmail.com',
'protocol': 'http',
})
return optional_kwargs
def get_config_patterns(self):
patterns = super(LocalHandler, self).get_config_patterns()
patterns += (
('domain', '%(sitename)s.localhost'),
('base_url', '%(protocol)s://%(domain)s'),
('db_name', '%(sitename)s'),
('db_user', '%(sitename)s'),
('log_path', '/var/log/django/%(sitename)s'),
('static_path', '/opt/var/www/%(sitename)s/static'),
('media_path', '/opt/var/www/%(sitename)s/uploads'),
('virtualenv_path', '/opt/.virtualenvs/%(sitename)s'),
('project_path', '/opt/www/%(sitename)s'),
('server_email', 'contact@%(domain)s'),
)
return patterns
def adjust(self):
super(LocalHandler, self).adjust()
self._settings['EMAIL_USE_TLS'] = self.config.email_use_tls
self._settings['EMAIL_PORT'] = self.config.email_port
self._settings['BASE_URL'] = self.config.base_url
INSTALLATIONS = [
LocalHandler('mysite'),
]
Sharing settings across projects
https://github.com/seddonym/bobsleigh-seddonym/
# project.py
from bobsleigh_seddonym.settings.base import *
INSTALLED_APPS += [
'myapp',
]
# installations.py
from bobsleigh_seddonym.handlers.local import LocalHandler
from bobsleigh_seddonym.handlers.webfaction import DevHandler, LiveHandler
INSTALLATIONS = [
LocalHandler(sitename='mysite'),
DevHandler(sitename='dev',
host='web123.webfaction.com',
webfaction_user='myuser',
domain='dev.mysite.com',
server_email='Mysite <contact@dev.mysite.com>',
prefixed_name='mysite_dev'),
LiveHandler(sitename='live',
host='web123.webfaction.com',
webfaction_user='myuser',
domain='mysite.com',
server_email='Mysite <contact@mysite.com>',
prefixed_name='mysite_live'),
]
An example project
In Conclusion...
Class Based Settings make more sense.
They may be more complex under the hood, but they allow you to decouple
your settings logic from your actual settings.
Could this be the Holy Grail?
Questions and comments?
David Seddon
seddonym.me
github.com/seddonym
david@seddonym.me
I don't really tweet
The Other True Way
By David Seddon
The Other True Way
A case for Class Based Settings
- 1,458