Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8a95e77
Custom EnvironmentVariableLoader for more advanced logic
doismellburning Jan 15, 2015
1d7b528
Moved debugenv into tests.env
doismellburning Jan 15, 2015
eca6bef
Example EVL usage
doismellburning Jan 15, 2015
8901cb4
Custom default value
doismellburning Jan 16, 2015
14404f0
Drop unused **kwargs argument
doismellburning Jan 16, 2015
e8b963d
Add custom parser for loading non-string values
doismellburning Jan 16, 2015
7c2e69c
Move ALLOWED_HOST parsing to EVL
doismellburning Jan 16, 2015
9ac7e4b
Documentation for EVL
doismellburning Jan 17, 2015
2f4c499
Documentation
doismellburning Jan 17, 2015
cd6d9a5
One day RST will become intuitive...
doismellburning Jan 17, 2015
c229a77
Merge branch 'master' into feature/custom-loader
doismellburning Jan 17, 2015
5ba4680
Placate doc8
doismellburning Jan 17, 2015
7694f89
parser, not loader
doismellburning Jan 17, 2015
36e37bb
Move DATABASE_URL parsing to EVL-using function
doismellburning Feb 7, 2015
0a0be27
Use numeric port; dj_database_url cares about this
doismellburning Feb 7, 2015
801e3e4
Don't assign lambda, to placate flake8
doismellburning Feb 7, 2015
fda6ba6
Merge branch 'master' into feature/custom-loader
doismellburning Feb 7, 2015
e861b26
Merge branch 'feature/custom-loader' into feature/evl-djdb
doismellburning Feb 7, 2015
1ae0656
Merge branch 'master' into feature/evl-djdb
doismellburning Feb 14, 2015
4d6e9bd
Merge branch 'master' into feature/custom-loader
doismellburning Mar 8, 2015
5fb1b08
Docstrings to plcate pep257
doismellburning Mar 9, 2015
9d2a87f
Line length limits are a thing
doismellburning Mar 9, 2015
df02aa7
Merge branch 'feature/custom-loader' into feature/evl-djdb
doismellburning Mar 9, 2015
3d69fc3
Merge branch 'master' into feature/custom-loader
doismellburning Apr 25, 2015
3a2edee
Merge branch 'feature/custom-loader' into feature/evl-djdb
doismellburning Apr 25, 2015
fe57c05
Merge pull request #22 from doismellburning/feature/evl-djdb
doismellburning Feb 5, 2016
d8d7bbe
Merge branch 'master' into feature/custom-loader
doismellburning Feb 9, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions django12factor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
import six
import sys

from .environment_variable_loader import EnvironmentVariableLoader

logger = logging.getLogger(__name__)


_FALSE_STRINGS = [
"no",
"false",
Expand Down Expand Up @@ -71,7 +74,7 @@ def factorise(custom_settings=None):
}

settings['DATABASES'] = {
'default': dj_database_url.config(default='sqlite://:memory:')
'default': _database()
}

for (key, value) in six.iteritems(os.environ):
Expand Down Expand Up @@ -123,13 +126,35 @@ def factorise(custom_settings=None):

settings.update(dj_email_url.config(default='dummy://'))

settings['ALLOWED_HOSTS'] = os.getenv('ALLOWED_HOSTS', '').split(',')
settings['ALLOWED_HOSTS'] = _allowed_hosts()

# For keys to different apis, etc.
if custom_settings is None:
custom_settings = []

for cs in custom_settings:
settings[cs] = os.getenv(cs)
if not isinstance(cs, EnvironmentVariableLoader):
cs = EnvironmentVariableLoader(cs)
settings[cs.name] = cs.load_value()

return settings


def _allowed_hosts():
e = EnvironmentVariableLoader(
"ALLOWED_HOSTS",
parser=lambda x: x.split(","),
default=[]
)

return e.load_value()


def _database():
e = EnvironmentVariableLoader(
"DATABASE_URL",
parser=dj_database_url.parse,
default=dj_database_url.parse('sqlite://:memory:')
)

return e.load_value()
54 changes: 54 additions & 0 deletions django12factor/environment_variable_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Utility class for transforming environment variables into Django settings.
"""

import os


def identity(x):
"""
Because Python's stdlib doesn't have an identity function.
"""

return x


class EnvironmentVariableLoader(object):
"""
Load an environment variable, for a flexible value of load.

Provide an (optional) parser function (e.g. `int`), and an (optional)
default.
"""

def __init__(self, name, default=None, parser=identity):
"""
Build a loader for an environment variable.

* `name`: (required) the environment key to use
* `default`: a value to use if the environment variable is not set
* `loader`: a function that takes a string environment value and
performs type conversion / parsing / etc. (e.g. `int` or
`lambda x: x.split(',')`)

Note there is no checking to ensure the type of `default` is the same
as the return type of `loader`, but if this isn't the case, you're
probably going to have a sad time.
"""

self.name = name
self.default = default
self.parser = parser

def load_value(self):
"""
Load and process an environment variable.
"""

if self.name in os.environ:
return self.parser(os.environ[self.name])
else:
return self.default


EVL = EnvironmentVariableLoader
28 changes: 28 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,34 @@ Treats ``os.environ("ALLOWED_HOSTS")`` as a comma-separated list.

Uses ``os.environ("SECRET_KEY")`` - required if ``DEBUG==False``.

Custom Settings
---------------

You can make ``django12factor`` load arbitrary settings from environment
variables with the ``custom_setting`` ``kwarg``. This takes an iterable of
``django12factor.EnvironmentVariableLoader`` instances (also importable as
``django12factor.EVL`` for brevity), or strings.

``EnvironmentVariableLoader`` instances are created with a ``name`` - the name
of the environment variable to load, an optional ``default`` for the case when
the environment variable is not set, and an optional ``parser`` - a callable
that should take the string value of an environment variable and convert it as
desired for your settings.

For example:

.. code-block: python::

from django12factor import EVL

custom_settings = (
EVL("API_HOST", default="localhost"),
EVL("API_PORT", default=8080, parser=int),
)

For brevity and compatibility, any ``custom_setting`` that is a string will be
treated as the name of an ``EnvironmentVariableLoader``.


Indices and tables
==================
Expand Down
5 changes: 5 additions & 0 deletions tests/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ def __enter__(self):
def __exit__(self, type, value, traceback):
os.environ = self.oldenviron


def debugenv(**kwargs):
return env(DEBUG="true", **kwargs)


env = Env
32 changes: 27 additions & 5 deletions tests/test_d12f.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import django12factor
import unittest

from .env import env
from .env import (
debugenv,
env,
)

d12f = django12factor.factorise


def debugenv(**kwargs):
return env(DEBUG="true", **kwargs)


class TestD12F(unittest.TestCase):

def test_object_no_secret_key_prod(self):
Expand Down Expand Up @@ -66,6 +65,29 @@ def test_missing_custom_keys(self):
self.assertEquals(present, settings['PRESENT'])
self.assertIsNone(settings['MISSING'])

def test_allowed_hosts(self):
with debugenv(ALLOWED_HOSTS="a"):
self.assertEquals(d12f()['ALLOWED_HOSTS'], ["a"])

with debugenv(ALLOWED_HOSTS="a,b"):
self.assertEquals(d12f()['ALLOWED_HOSTS'], ["a", "b"])

with debugenv():
self.assertEquals(d12f()["ALLOWED_HOSTS"], [])

def test_djdb(self):
"""
Assert basic behaviour about DATABASE_URL parsing and defaults
"""
def defaultdb():
return d12f()['DATABASES']['default']

with debugenv():
self.assertEquals(defaultdb()['NAME'], ":memory:")

with debugenv(DATABASE_URL="postgres://USER:PASSWORD@HOST:9/NAME"):
self.assertEquals(defaultdb()['NAME'], "NAME")

def test_multiple_db_support(self):
"""
Explicit test that multiple DATABASE_URLs are supported.
Expand Down
53 changes: 53 additions & 0 deletions tests/test_evl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import django12factor
import unittest

from .env import (
debugenv,
)
from django12factor.environment_variable_loader import EVL

d12f = django12factor.factorise


class TestEVL(unittest.TestCase):

def test_equivalence(self):
with debugenv(FOO='x'):
custom_string = d12f(custom_settings=['FOO'])

with debugenv(FOO='x'):
custom_evl = d12f(custom_settings=[EVL("FOO")])

self.assertEqual(custom_string, custom_evl)

def test_default_is_none(self):
with debugenv():
key = "FOO"
d = d12f(custom_settings=[EVL(key)])
self.assertIsNone(
d[key],
"d12f tried to load the (unset) environment variable '%s' "
"with no default; expected None but got %s instead" %
(key, str(d[key]))
)

def test_custom_default(self):
with debugenv():
key = "FOO"
default = "llama"
d = d12f(custom_settings=[EVL(key, default=default)])
self.assertEquals(
default, d[key],
"d12f tried to load the (unset) environment variable '%s' "
"with a default of %s, but got %s instead" %
(key, str(default), str(d[key]))
)

def test_loading_int(self):
key = "FOO"
value = 1
with debugenv(**{key: str(value)}):
d = d12f(custom_settings=[EVL(key, parser=int)])
i = d[key]
self.assertEquals(i, value)
self.assertIsInstance(i, int)