Rob Golding

Technology Consultant

Extending Settings Variables with local_settings.py in Django

I discovered this hacky way to use the local_settings trick to extend and/or override values in the main Django settings file today. Some projects use a "reverse" version of the local_settings trick (which is explained below), whereby the main settings file becomes settings_local.py or something similar, which first imports settings.py, and then extends or overrides the values as required.

I didn't want to change the name of the project settings file to settings_local, however, as it would mean changing the WSGI file on every server that the project runs on.

The local_settings Trick

It's a well-known trick to use a file called local_settings.py or something similar, with a piece of code at the bottom of the main settings file:

try:
    from local_settings import *
except:
    pass

to override the value of settings variables. This will not work, however, if you wish to extend a settings variable (for example, adding an app to INSTALLED_APPS). For this, I have found that the following ugly hack seems to do the job.

The Ugly Hack

Replace the snippet at the bottom of the main settings file with the following code

try:
    LOCAL_SETTINGS
except NameError:
    try:
        from local_settings import *
    except ImportError:
        pass

What this is doing is effectively checking for the presence of a variable called LOCAL_SETTINGS. The local_settings file then contains this code:

LOCAL_SETTINGS = True
from settings import *

This means that the local_settings file will only be imported once, and it has all the variables in the main settings file available to extend at will. For example, to enable the Django debug toolbar:

MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',)
INSTALLED_APPS += ('debug_toolbar',)

The code is ugly (and results in the main settings file being parsed twice) but it works!

Categories: Technology, Web Development, django, programming | Tags: , , | RSS 2.0

  • Another way to solve this is to have a local import in local_settings for settings. Example local_settings.py:



    def get_setting(setting):
    import settings
    return getattr(settings, setting)

    INSTALLED_APPS = get_setting('INSTALLED_APPS') + ('debug_toolbar', )
    MIDDLEWARE_CLASSES = get_setting('MIDDLEWARE_CLASSES') + ('debug_toolbar.middleware.DebugToolbarMiddleware',)
    INTERNAL_IPS = ('127.0.0.1',)
  • Thanks Antti, I've never seen that page before. There's some pretty interesting solutions on there.



    I decided to go with Harro's solution of using a default_settings.py file, along with a local_settings.py to override those values. It works nicely, and the right things stay out of version control.

  • Antti Kaihola

    See also http://code.djangoproject.com/wiki/SplitSettings for a collection of solutions.

  • Rather than having a local_settings.py that could override an unknown group of settings, I use a more deterministic setup. In settings.py, I pull specific settings from local_settings, requiring all local_settings.py variations to declare all potentially modifiable values.



    For example, in settings.py:

    import local_settings
    DEBUG = local_settings.DEBUG



    I find this to be a little more robust that the commonly used local_settings trick. It helps me to avoid mistakes like forgetting local_settings when deploying to a new server. Rather than having the app accidentally run with DEBUG set to True (bad practice in itself to have it set to true in settings.py, but I digress), the app will simply fail to run because local_settings does not exist.



    Plus, that way, I have no need for local_settings.py to reference anything from settings.py. It should only need things contained within itself or from Django's default settings.

  • Mike Dewhirst

    This is slightly off-topic but I try to version everything except credentials which I keep in a separate directory well clear of the development area. I can put all creds in one place in files with a .cred extension and just fetch them when required like this ...



    credsdir = '/srv/www/' + APP + '/creds/'
    creds = credsdir + 'database.cred'
    def getcreds(fname=creds):
    """
    This is only for keeping credentials out of the repository when
    settings.py is under version control provided always that CREDS itself
    isn't versioned!!! creds still needs to be invisible in the filesystem
    to every user except the http server - chmod 0700. chown wwwrun:www
    The first line of creds is userid, next is password and
    subsequent lines can contain comments or anything really.
    """
    fh = open(fname, 'r')
    cred = [fh.readline().strip(), fh.readline().strip()]
    fh.close()
    return cred


    Then cred[0] is the userid and cred[1] is the password

  • That's a really clever solution, I haven't seen that one before. Thanks! I'll give it a go.

    Rob
  • Harro

    Do as Daniel says an move the settings to default settings.
    Then in the default settings do:



    try:
    from local_settings import *
    except ImportError
    from default_settings import *


    And in your local_settings do a:



    from default_settings import *


    Then you can put stuff in the local settings and everything will still work even if there are no local settings

  • Nice idea, and it's certainly a lot neater than my solution :). I may give it a go. It does mean though, that I have to version control the customised settings file, which I don't really want to do (as database passwords and the like live in there).

    Cheers
  • Daniel Swarbrick
    Err, that should be "from default_settings import *" in my comment above.
  • Daniel Swarbrick

    Why not simply rename the original settings.py to something like default_settings.py, then create a new, empty settings.py with the following in it:



    from default_settings.py import *
    MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
    INTERNAL_IPS = ('127.0.0.1',)
    INSTALLED_APPS += ('debug_toolbar',)
    MY_CUSTOM_PARAM = 'foo'

  • Hi,
    I use a different techinique.
    At the end of my setting file I put this:



    from conf import *


    "Conf" is a folder/module inside the same folder with this inside:



    conf/
    __init__.py
    development.py
    staging.py
    production.py


    The init.py looks like this:



    """
    The HOSTMAP variable is a dictionary of lists, the keys representing
    roles of a server, the values representing the hostnames of machines that
    fill those roles.
    You can get your hostname by typing `hostname` into a terminal.
    """


    -
    import socket, warnings, sys
    from django.utils.importlib import import_module
    HOSTMAP = {
    'development': [
    'iRule',
    'rfc-1918',
    'ubuntu',
    ],
    'staging': [
    'vps2728.webhosting.uk.com',
    ],
    'production': [
    # 'vps2728.webhosting.uk.com',
    ],
    }



    def update_current_settings(file_name):
    """
    Given a filename, this function will insert all variables and
    functions in ALL_CAPS into the global scope.
    """
    new_settings = import_module(file_name)

    for k, v in new_settings.__dict__.items():
    if k.upper() == k:
    globals().update({k:v})

    current_hostname = socket.gethostname()
    to_load = []

    for k, v in HOSTMAP.items():
    if current_hostname in v:
    to_load.append(k)

    for x in to_load:
    new_settings_path= 'brokenseal.conf.%s' % x
    try:
    update_current_settings(new_settings_path)
    except ImportError:
    warnings.warn("Failed to import %s" % new_settings_path)


    This way, no matter where I am, the right settings will be imported.



    Have a look here: http://github.com/brokenseal/brokenseal
    and here: https://gist.github.com/217198/1e3b5e44b23abcfe0cb57c6470c832cd0b23e83f



    Cheers,
    David

blog comments powered by Disqus