'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

  1. Easy deployment: project-level config stored in codebase
     
  2. Security: sensitive settings stored locally
     
  3. 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

Made with Slides.com