From 8a95e777736a6957636a1b37c62f38d10fff731d Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Thu, 15 Jan 2015 18:23:03 +0000 Subject: [PATCH 01/17] Custom EnvironmentVariableLoader for more advanced logic Massively prototypey --- django12factor/__init__.py | 7 ++++++- django12factor/environment_variable_loader.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 django12factor/environment_variable_loader.py diff --git a/django12factor/__init__.py b/django12factor/__init__.py index 947ab52..4dc2adc 100644 --- a/django12factor/__init__.py +++ b/django12factor/__init__.py @@ -5,8 +5,11 @@ import logging import sys +from .environment_variable_loader import EnvironmentVariableLoader + logger = logging.getLogger(__name__) + _FALSE_STRINGS = [ "no", "false", @@ -81,6 +84,8 @@ def factorise(custom_settings=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 diff --git a/django12factor/environment_variable_loader.py b/django12factor/environment_variable_loader.py new file mode 100644 index 0000000..2385339 --- /dev/null +++ b/django12factor/environment_variable_loader.py @@ -0,0 +1,12 @@ +import os + + +class EnvironmentVariableLoader(object): + def __init__(self, name, **kwargs): + self.name = name + + def load_value(self): + return os.getenv(self.name) + + +EVL = EnvironmentVariableLoader From 1d7b528a7da32bf161013106b5053ecc1f30ff9f Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Thu, 15 Jan 2015 22:41:17 +0000 Subject: [PATCH 02/17] Moved debugenv into tests.env --- tests/env.py | 5 +++++ tests/test_d12f.py | 9 ++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/env.py b/tests/env.py index 41302ba..53bb951 100644 --- a/tests/env.py +++ b/tests/env.py @@ -12,4 +12,9 @@ def __enter__(self): def __exit__(self, type, value, traceback): os.environ = self.oldenviron + +def debugenv(**kwargs): + return env(DEBUG="true", **kwargs) + + env = Env diff --git a/tests/test_d12f.py b/tests/test_d12f.py index 193b837..a73a4d1 100644 --- a/tests/test_d12f.py +++ b/tests/test_d12f.py @@ -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): From eca6bef6166b026036e51daac02b64b543edef41 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Thu, 15 Jan 2015 23:41:13 +0000 Subject: [PATCH 03/17] Example EVL usage --- tests/test_evl.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/test_evl.py diff --git a/tests/test_evl.py b/tests/test_evl.py new file mode 100644 index 0000000..3e92ff2 --- /dev/null +++ b/tests/test_evl.py @@ -0,0 +1,21 @@ +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) From 8901cb43c5629fd6d199216e2ed66beb88cbe3fc Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Fri, 16 Jan 2015 00:05:18 +0000 Subject: [PATCH 04/17] Custom default value --- django12factor/environment_variable_loader.py | 5 ++-- tests/test_evl.py | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/django12factor/environment_variable_loader.py b/django12factor/environment_variable_loader.py index 2385339..cff7ea1 100644 --- a/django12factor/environment_variable_loader.py +++ b/django12factor/environment_variable_loader.py @@ -2,11 +2,12 @@ class EnvironmentVariableLoader(object): - def __init__(self, name, **kwargs): + def __init__(self, name, default=None, **kwargs): self.name = name + self.default = default def load_value(self): - return os.getenv(self.name) + return os.getenv(self.name, default=self.default) EVL = EnvironmentVariableLoader diff --git a/tests/test_evl.py b/tests/test_evl.py index 3e92ff2..a8601e2 100644 --- a/tests/test_evl.py +++ b/tests/test_evl.py @@ -19,3 +19,26 @@ def test_equivalence(self): 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])) + ) From 14404f017d5b99a54fced1b4a9d440f0dd6855db Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Fri, 16 Jan 2015 09:40:28 +0000 Subject: [PATCH 05/17] Drop unused **kwargs argument --- django12factor/environment_variable_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django12factor/environment_variable_loader.py b/django12factor/environment_variable_loader.py index cff7ea1..d4d8399 100644 --- a/django12factor/environment_variable_loader.py +++ b/django12factor/environment_variable_loader.py @@ -2,7 +2,7 @@ class EnvironmentVariableLoader(object): - def __init__(self, name, default=None, **kwargs): + def __init__(self, name, default=None): self.name = name self.default = default From e8b963d16d0e4727a50a06d9ee29edf2cad90b65 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Fri, 16 Jan 2015 09:42:28 +0000 Subject: [PATCH 06/17] Add custom parser for loading non-string values --- django12factor/environment_variable_loader.py | 11 +++++++++-- tests/test_evl.py | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/django12factor/environment_variable_loader.py b/django12factor/environment_variable_loader.py index d4d8399..47020bf 100644 --- a/django12factor/environment_variable_loader.py +++ b/django12factor/environment_variable_loader.py @@ -1,13 +1,20 @@ import os +identity = lambda x: x + + class EnvironmentVariableLoader(object): - def __init__(self, name, default=None): + def __init__(self, name, default=None, parser=identity): self.name = name self.default = default + self.parser = parser def load_value(self): - return os.getenv(self.name, default=self.default) + if self.name in os.environ: + return self.parser(os.environ[self.name]) + else: + return self.default EVL = EnvironmentVariableLoader diff --git a/tests/test_evl.py b/tests/test_evl.py index a8601e2..45b202c 100644 --- a/tests/test_evl.py +++ b/tests/test_evl.py @@ -42,3 +42,12 @@ def test_custom_default(self): "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) From 7c2e69cf6ae975db070f66b0468bfe8de47b9c58 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Fri, 16 Jan 2015 16:22:43 +0100 Subject: [PATCH 07/17] Move ALLOWED_HOST parsing to EVL This changes behaviour - now if ALLOWED_HOSTS is not set in the environment, you get an empty list; previously you got a list containing the empty string. This is essentially a bug fix - `[]` is a much more sensible value than `['']` --- django12factor/__init__.py | 12 +++++++++++- tests/test_d12f.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/django12factor/__init__.py b/django12factor/__init__.py index 4dc2adc..8827673 100644 --- a/django12factor/__init__.py +++ b/django12factor/__init__.py @@ -77,7 +77,7 @@ 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: @@ -89,3 +89,13 @@ def factorise(custom_settings=None): 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() diff --git a/tests/test_d12f.py b/tests/test_d12f.py index a73a4d1..4e7b573 100644 --- a/tests/test_d12f.py +++ b/tests/test_d12f.py @@ -64,3 +64,13 @@ def test_missing_custom_keys(self): settings = d12f(['PRESENT', 'MISSING']) 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"], []) From 9ac7e4b16188e46e903724c9f965a27906dfe156 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Sat, 17 Jan 2015 01:18:30 +0000 Subject: [PATCH 08/17] Documentation for EVL --- django12factor/environment_variable_loader.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/django12factor/environment_variable_loader.py b/django12factor/environment_variable_loader.py index 47020bf..81acc83 100644 --- a/django12factor/environment_variable_loader.py +++ b/django12factor/environment_variable_loader.py @@ -6,6 +6,19 @@ class EnvironmentVariableLoader(object): def __init__(self, name, default=None, parser=identity): + """ + Builds 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 From 2f4c49948240c0cce037f4fee1bb6122edb5238e Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Sat, 17 Jan 2015 14:11:00 +0000 Subject: [PATCH 09/17] Documentation (Poorly-worded, but it exists) --- README.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.rst b/README.rst index 4fbf064..218a61f 100644 --- a/README.rst +++ b/README.rst @@ -129,3 +129,24 @@ 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 ``loader`` - 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, loader=int), + ) + +For brevity and compatibility, any ``custom_setting`` that is a string will be treated as the name of an ``EnvironmentVariableLoader``. From cd6d9a53b2418243c421892bba44e505fe303393 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Sat, 17 Jan 2015 14:13:36 +0000 Subject: [PATCH 10/17] One day RST will become intuitive... --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 218a61f..d45de7a 100644 --- a/README.rst +++ b/README.rst @@ -142,6 +142,7 @@ This takes an iterable of ``django12factor.EnvironmentVariableLoader`` instances For example: .. code-block: python:: + from django12factor import EVL custom_settings = ( From 5ba46809b0f108ce7f5b0f6fbdc192a8bffd305d Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Sat, 17 Jan 2015 15:41:59 +0000 Subject: [PATCH 11/17] Placate doc8 --- README.rst | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 42810ce..1ed0e79 100644 --- a/README.rst +++ b/README.rst @@ -138,10 +138,16 @@ 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 +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 ``loader`` - a callable that should take the string value of an environment variable and convert it as desired for your settings. +``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 ``loader`` - a callable +that should take the string value of an environment variable and convert it as +desired for your settings. For example: @@ -154,4 +160,5 @@ For example: EVL("API_PORT", default=8080, loader=int), ) -For brevity and compatibility, any ``custom_setting`` that is a string will be treated as the name of an ``EnvironmentVariableLoader``. +For brevity and compatibility, any ``custom_setting`` that is a string will be +treated as the name of an ``EnvironmentVariableLoader``. From 7694f8956e22a7052e0a4d0e3982d96355106f4e Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Sat, 17 Jan 2015 15:46:59 +0000 Subject: [PATCH 12/17] parser, not loader --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1ed0e79..1d93050 100644 --- a/README.rst +++ b/README.rst @@ -145,7 +145,7 @@ variables with the ``custom_setting`` ``kwarg``. This takes an iterable of ``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 ``loader`` - a callable +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. @@ -157,7 +157,7 @@ For example: custom_settings = ( EVL("API_HOST", default="localhost"), - EVL("API_PORT", default=8080, loader=int), + EVL("API_PORT", default=8080, parser=int), ) For brevity and compatibility, any ``custom_setting`` that is a string will be From 36e37bbe6727dd9507c08a95faa3135077f222c0 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Sat, 7 Feb 2015 22:39:29 +0000 Subject: [PATCH 13/17] Move DATABASE_URL parsing to EVL-using function --- django12factor/__init__.py | 12 +++++++++++- tests/test_d12f.py | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/django12factor/__init__.py b/django12factor/__init__.py index 8827673..f5e5899 100644 --- a/django12factor/__init__.py +++ b/django12factor/__init__.py @@ -54,7 +54,7 @@ def factorise(custom_settings=None): } settings['DATABASES'] = { - 'default': dj_database_url.config(default='sqlite://:memory:') + 'default': _database() } settings['DEBUG'] = getenv_bool('DEBUG') @@ -99,3 +99,13 @@ def _allowed_hosts(): ) 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() diff --git a/tests/test_d12f.py b/tests/test_d12f.py index 4e7b573..6239d57 100644 --- a/tests/test_d12f.py +++ b/tests/test_d12f.py @@ -74,3 +74,16 @@ def test_allowed_hosts(self): 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:PORT/NAME"): + self.assertEquals(defaultdb()['NAME'], "NAME") From 0a0be279e4bdc20faa95110d94a731aba6573281 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Sat, 7 Feb 2015 22:41:01 +0000 Subject: [PATCH 14/17] Use numeric port; dj_database_url cares about this --- tests/test_d12f.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_d12f.py b/tests/test_d12f.py index 6239d57..972e520 100644 --- a/tests/test_d12f.py +++ b/tests/test_d12f.py @@ -85,5 +85,5 @@ def defaultdb(): with debugenv(): self.assertEquals(defaultdb()['NAME'], ":memory:") - with debugenv(DATABASE_URL="postgres://USER:PASSWORD@HOST:PORT/NAME"): + with debugenv(DATABASE_URL="postgres://USER:PASSWORD@HOST:9/NAME"): self.assertEquals(defaultdb()['NAME'], "NAME") From 801e3e4b83969e77338bab4083e6fde568c59ae1 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Sat, 7 Feb 2015 22:56:40 +0000 Subject: [PATCH 15/17] Don't assign lambda, to placate flake8 --- django12factor/environment_variable_loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django12factor/environment_variable_loader.py b/django12factor/environment_variable_loader.py index 81acc83..4e767ea 100644 --- a/django12factor/environment_variable_loader.py +++ b/django12factor/environment_variable_loader.py @@ -1,7 +1,8 @@ import os -identity = lambda x: x +def identity(x): + return x class EnvironmentVariableLoader(object): From 5fb1b08ee5dcf265bbc1e7430c404bad5d269309 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Mon, 9 Mar 2015 00:08:57 +0000 Subject: [PATCH 16/17] Docstrings to plcate pep257 --- django12factor/environment_variable_loader.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/django12factor/environment_variable_loader.py b/django12factor/environment_variable_loader.py index 4e767ea..ea96cb0 100644 --- a/django12factor/environment_variable_loader.py +++ b/django12factor/environment_variable_loader.py @@ -1,14 +1,28 @@ +""" +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): """ - Builds a loader for an environment variable. + 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 @@ -20,11 +34,16 @@ def __init__(self, name, default=None, parser=identity): 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: From 9d2a87f22e729f14f7d77e8cba71d6b170a9ada8 Mon Sep 17 00:00:00 2001 From: Kristian Glass Date: Mon, 9 Mar 2015 00:12:02 +0000 Subject: [PATCH 17/17] Line length limits are a thing --- django12factor/environment_variable_loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django12factor/environment_variable_loader.py b/django12factor/environment_variable_loader.py index ea96cb0..296cca8 100644 --- a/django12factor/environment_variable_loader.py +++ b/django12factor/environment_variable_loader.py @@ -17,7 +17,8 @@ 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. + Provide an (optional) parser function (e.g. `int`), and an (optional) + default. """ def __init__(self, name, default=None, parser=identity):