/usr/share

Dabblings in the webs and suchlike.

Posts tagged python

Aug 4

A Fairly Low Ceremony Method for Modularizing Django Settings

Updates

  • No longer wrapping modules in def require
  • Using pkgutil and inspect to get module names and constants

Every time I need to add anything to settings.py in Django, I find myself spending a good minute trying to find a logical place to put it. But it’s always the same. I give up, put it somewhere arbitrary, and move on feeling cheapened, dirty, and defeated. There must be a better way…

This wiki page provides a ton of examples that get very close. They let you override settings per environment, but you’re still arbitrarily placing unrelated settings together. I’d like something more modular. Today I worked out a method that is very modular and includes all the perks of the above examples at the cost of a small bit of ceremony.

For reference, take a look at the updated example project settings. The gist of it is moving settings into it’s own package, importing all settings in __init__.py, and placing specific settings into aptly named modules.

Version 2.0

import inspect
import pkgutil


for _, module_name, _ in pkgutil.walk_packages(__path__):
    module = __import__(module_name, globals(), locals(), [])
    for var_name, val in inspect.getmembers(module):
        if var_name.isupper():
            locals().update({var_name: val})

This version improves on the first by getting module names using pkgutil.walk_packages which means you can put settings in packages now too.

Also, instead of setting up the secrets, paths, and environment in here and passing them via require I let the settings modules import the settings they need.

import os

import env
import paths
import secrets


if env.DEV_ENV:
    database_dir = os.path.join(paths.BASE_PATH, 'databases')

    if not os.path.exists(database_dir):
        os.mkdir(database_dir)

    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': os.path.join(database_dir, 'sqlite3'),
        }
    }
else:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql_psycopg2',
            'USER': secrets.DATABASE_USER,
            'PASSWORD': secrets.DATABASE_PASSWORD,
            'HOST': secrets.DATABASE_HOST,
            'PORT': secrets.DATABASE_PORT,
        }
    }

Instead of import from I just import the modules directly and grab the settings from them. This makes things a bit more explicit and ensures the imported settings don’t get added twice in __init__.py.

Extending settings is exactly the same as version 1.0:

import env


INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
)

if env.DEV_ENV:
    INSTALLED_APPS += (
        'debug_toolbar',
    )

So in this version we’ve cleaned up __init__.py quite a bit and removed the ceremony from the settings modules. This is exactly what I wanted from the get go and I’m pretty stinkin happy with it.

Version 1.0

import os

from secrets import *

SETTINGS_PATH = os.path.abspath(os.path.dirname(__file__))
PROJECT_PATH  = os.path.dirname(SETTINGS_PATH)
BASE_PATH     = os.path.dirname(PROJECT_PATH)

ENV      = os.getenv('DJANGO_ENVIRONMENT', 'development')
DEV_ENV  = ENV == 'development'
TEST_ENV = ENV == 'staging'
PROD_ENV = ENV == 'production'


def _constants(d):
    return dict((key, val) for key, val in d.items() if key.isupper())

for settings_file in [filename.rstrip('.py') for filename in os.listdir(SETTINGS_PATH)
        if not filename.endswith('.pyc') and filename not in ('__init__.py', 'secrets.py')]:

    module = __import__(settings_file, globals(), locals(), [])
    settings = module.require(**_constants(locals()))
    locals().update(_constants(settings))

The first thing we do is import secrets.py. This is where you’ll put the SECRET_KEY, database credentials, etc, and is the standard of the wiki examples. Next, we setup our project paths and the environment variable. But from here I stray from the norm a bit. Instead of importing a module based on the environment, I go ahead and import everything in the settings package (listdir may not be the most elegant way to get each filename, let me know if there’s a better approach).

I quickly ran into a problem though. I want to use the secrets, project paths, and environment settings inside the imported modules. This is where the ceremony comes in. To get around this I wrap each settings module in a function I can call in __init__.py, passing in the settings it needs like so:

import os

def require(DEV_ENV, BASE_PATH, DATABASE_USER, DATABASE_PASSWORD,
        DATABASE_HOST, DATABASE_PORT, **kwargs):

    if DEV_ENV:
        database_dir = os.path.join(BASE_PATH, 'databases')

        if not os.path.exists(database_dir):
            os.mkdir(database_dir)

        DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': os.path.join(database_dir, 'sqlite3'),
            }
        }
    else:
        DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.postgresql_psycopg2',
                'USER': DATABASE_USER,
                'PASSWORD': DATABASE_PASSWORD,
                'HOST': DATABASE_HOST,
                'PORT': DATABASE_PORT,
            }
        }

    return locals()

So each module must def require (hat tip to RequireJS) with **kwargs and return locals(). Not too bad. Also, the above example shows another way in which I diverge from the wiki examples. Instead of overriding per environment, I just check the environment setting and drop in the correct setting. To extend settings per environment you would do the following:

def require(DEV_ENV, **kwargs):

    INSTALLED_APPS = (
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.sites',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        # Uncomment the next line to enable the admin:
        # 'django.contrib.admin',
        # Uncomment the next line to enable admin documentation:
        # 'django.contrib.admindocs',
    )

    if DEV_ENV:
        INSTALLED_APPS += (
            'debug_toolbar',
        )

    return locals()

All in all, I feel like this is a step in the right direction. If the settings package crammed full of sparse modules makes you queazy, than stick with one of the wiki examples. But for me, the ability to drop in a widgery.py module when I want to configure my widgery app is a breath of fresh air. Everything finally has a place.

Let me know what you think!


Apr 27

Vim Python Complete Function

I finally found a nice complete function setup for python in vim. It’s simply using rope-vim’s code assistance but there are a few settings you need to enable. I should note that I was clued in on rope and a ton of other great plugins through a nice article by John Anderson on using vim as a python ide. You can follow his setup or I use janus so I dropped them into my janus folder as git submodules. The rope-vim plugin he suggests includes all the rope packages so you don’t need to install them yourself.

In the code assist section of :h ropevim.txt, you’ll see that you can set a few globals to have rope-vim handle completion:

let g:ropevim_vim_completion=1
let g:ropevim_extended_complete=1

The first setting makes the RopeCodeAssistInsertMode function behave like a vim complete function, i.e. it display a popup menu when called. And the second adds useful bits of info to each completion: the location the completion came from {'L': 'local', 'G': 'global', 'B': 'builtin', '': 'no scope available'}, it’s type, and the first line of the doc string. It’s pretty sweet!

Other than that, you might want to remap the awkward default code assist mapping of alt+shift+comma (see the filetype commands of my vimrc for an option). Also, if you’ve never used rope before it will ask you to create or open a project each time. This stores a hidden ropeproject directory that caches info about your modules to make completions faster. It’s slightly annoying to open each time but it’s worth it for completions.

And that’s all there is to it!

vim python completion menu