Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Christian Abbott 2015-01-07 03:19:24 -08:00
commit 8287ab6f7f
20 changed files with 377 additions and 74 deletions

6
.coveragerc Normal file
View file

@ -0,0 +1,6 @@
[run]
source = configurations
branch = 1
[report]
omit = *tests*,*migrations*

1
.gitignore vendored
View file

@ -7,3 +7,4 @@ build/
.tox/
htmlcov/
*.pyc
dist/

View file

@ -1,6 +1,5 @@
language: python
env:
- TOXENV=coverage-py27-dj16
- TOXENV=flake8-py27
- TOXENV=flake8-py33
- TOXENV=py26-dj14

View file

@ -1,7 +1,6 @@
include README.rst
include AUTHORS
include .travis.yml
include manage.py
include tasks.py
recursive-include tests *
recursive-include docs *

View file

@ -13,3 +13,14 @@ def load_ipython_extension(ipython):
from . import importer
importer.install()
def setup(app):
"""
The callback for Sphinx that acts as a Sphinx extension.
Add this to the ``extensions`` config variable in your ``conf.py``.
"""
from . import importer
importer.install()

View file

@ -1,3 +1,5 @@
import os
import re
import warnings
from django.utils import six
@ -66,9 +68,53 @@ class Configuration(six.with_metaclass(ConfigurationBase)):
to the name of the class.
"""
DOTENV_LOADED = None
@classmethod
def load_dotenv(cls):
"""
Pulled from Honcho code with minor updates, reads local default
environment variables from a .env file located in the project root
or provided directory.
http://www.wellfireinteractive.com/blog/easier-12-factor-django/
https://gist.github.com/bennylope/2999704
"""
# check if the class has DOTENV set wether with a path or None
dotenv = getattr(cls, 'DOTENV', None)
# if DOTENV is falsy we want to disable it
if not dotenv:
return
# now check if we can access the file since we know we really want to
try:
with open(dotenv, 'r') as f:
content = f.read()
except IOError as e:
raise ImproperlyConfigured("Couldn't read .env file "
"with the path {}. Error: "
"{}".format(dotenv, e))
else:
for line in content.splitlines():
m1 = re.match(r'\A([A-Za-z_0-9]+)=(.*)\Z', line)
if not m1:
continue
key, val = m1.group(1), m1.group(2)
m2 = re.match(r"\A'(.*)'\Z", val)
if m2:
val = m2.group(1)
m3 = re.match(r'\A"(.*)"\Z', val)
if m3:
val = re.sub(r'\\(.)', r'\1', m3.group(1))
os.environ.setdefault(key, val)
cls.DOTENV_LOADED = dotenv
@classmethod
def pre_setup(cls):
pass
if cls.DOTENV_LOADED is None:
cls.load_dotenv()
@classmethod
def post_setup(cls):
@ -88,4 +134,4 @@ class Settings(Configuration):
# make sure to remove the handling of the Settings class above when deprecating
warnings.warn("configurations.Settings was renamed to "
"settings.Configuration and will be "
"removed in 1.0", PendingDeprecationWarning)
"removed in 1.0", DeprecationWarning)

View file

@ -5,10 +5,9 @@ import sys
from optparse import make_option
from django.core.exceptions import ImproperlyConfigured
from django.core.management import LaxOptionParser
from django.conf import ENVIRONMENT_VARIABLE as SETTINGS_ENVIRONMENT_VARIABLE
from .utils import uppercase_attributes, reraise
from .utils import uppercase_attributes, reraise, LaxOptionParser
from .values import Value, setup_value
installed = False

View file

@ -2,4 +2,5 @@ from . import importer
importer.install(check_options=True)
from django.core.management import execute_from_command_line # noqa
from django.core.management import (execute_from_command_line, # noqa
call_command)

View file

@ -1,3 +1,4 @@
import inspect
import sys
from django.core.exceptions import ImproperlyConfigured
@ -60,3 +61,138 @@ def reraise(exc, prefix=None, suffix=None):
suffix = '(' + suffix + ')'
exc.args = ('{0} {1} {2}'.format(prefix, exc.args[0], suffix),) + args[1:]
raise
try:
from django.core.management import LaxOptionParser
except ImportError:
from optparse import OptionParser
class LaxOptionParser(OptionParser):
"""
An option parser that doesn't raise any errors on unknown options.
This is needed because the --settings and --pythonpath options affect
the commands (and thus the options) that are available to the user.
Backported from Django 1.7.x
"""
def error(self, msg):
pass
def print_help(self):
"""Output nothing.
The lax options are included in the normal option parser, so under
normal usage, we don't need to print the lax options.
"""
pass
def print_lax_help(self):
"""Output the basic options available to every command.
This just redirects to the default print_help() behavior.
"""
OptionParser.print_help(self)
def _process_args(self, largs, rargs, values):
"""
Overrides OptionParser._process_args to exclusively handle default
options and ignore args and other options.
This overrides the behavior of the super class, which stop parsing
at the first unrecognized option.
"""
while rargs:
arg = rargs[0]
try:
if arg[0:2] == "--" and len(arg) > 2:
# process a single long option (possibly with value(s))
# the superclass code pops the arg off rargs
self._process_long_opt(rargs, values)
elif arg[:1] == "-" and len(arg) > 1:
# process a cluster of short options (possibly with
# value(s) for the last one only)
# the superclass code pops the arg off rargs
self._process_short_opts(rargs, values)
else:
# it's either a non-default option or an arg
# either way, add it to the args list so we can keep
# dealing with options
del rargs[0]
raise Exception
except: # Needed because we might need to catch a SystemExit
largs.append(arg)
# Copied over from Sphinx
if sys.version_info >= (3, 0):
from functools import partial
def getargspec(func):
"""Like inspect.getargspec but supports functools.partial as well."""
if inspect.ismethod(func):
func = func.__func__
if type(func) is partial:
orig_func = func.func
argspec = getargspec(orig_func)
args = list(argspec[0])
defaults = list(argspec[3] or ())
kwoargs = list(argspec[4])
kwodefs = dict(argspec[5] or {})
if func.args:
args = args[len(func.args):]
for arg in func.keywords or ():
try:
i = args.index(arg) - len(args)
del args[i]
try:
del defaults[i]
except IndexError:
pass
except ValueError: # must be a kwonly arg
i = kwoargs.index(arg)
del kwoargs[i]
del kwodefs[arg]
return inspect.FullArgSpec(args, argspec[1], argspec[2],
tuple(defaults), kwoargs,
kwodefs, argspec[6])
while hasattr(func, '__wrapped__'):
func = func.__wrapped__
if not inspect.isfunction(func):
raise TypeError('%r is not a Python function' % func)
return inspect.getfullargspec(func)
else: # 2.6, 2.7
from functools import partial
def getargspec(func):
"""Like inspect.getargspec but supports functools.partial as well."""
if inspect.ismethod(func):
func = func.im_func
parts = 0, ()
if type(func) is partial:
keywords = func.keywords
if keywords is None:
keywords = {}
parts = len(func.args), keywords.keys()
func = func.func
if not inspect.isfunction(func):
raise TypeError('%r is not a Python function' % func)
args, varargs, varkw = inspect.getargs(func.func_code)
func_defaults = func.func_defaults
if func_defaults is None:
func_defaults = []
else:
func_defaults = list(func_defaults)
if parts[0]:
args = args[parts[0]:]
if parts[1]:
for arg in parts[1]:
i = args.index(arg) - len(args)
del args[i]
try:
del func_defaults[i]
except IndexError:
pass
return inspect.ArgSpec(args, varargs, varkw, func_defaults)

View file

@ -8,7 +8,7 @@ from django.core import validators
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils import six
from .utils import import_by_path
from .utils import import_by_path, getargspec
def setup_value(target, name, value):
@ -44,7 +44,7 @@ class Value(object):
def __new__(cls, *args, **kwargs):
"""
checks if the creation can end up directly in the final value.
That is the case whenever environ = False or environ_name is given
That is the case whenever environ = False or environ_name is given.
"""
instance = object.__new__(cls)
instance.__init__(*args, **kwargs)
@ -59,7 +59,7 @@ class Value(object):
environ_prefix='DJANGO', *args, **kwargs):
if 'late_binding' in kwargs:
self.late_binding = kwargs.get('late_binding')
if isinstance(default, Value) and default.default:
if isinstance(default, Value) and default.default is not None:
self.default = copy.copy(default.default)
else:
self.default = default
@ -140,10 +140,20 @@ class CastingMixin(object):
error = 'Cannot use caster of {0} ({1!r})'.format(self,
self.caster)
raise ValueError(error)
try:
arg_names = getargspec(self._caster)[0]
self._params = dict((name, kwargs[name])
for name in arg_names
if name in kwargs)
except TypeError:
self._params = {}
def to_python(self, value):
try:
return self._caster(value)
if self._params:
return self._caster(value, **self._params)
else:
return self._caster(value)
except self.exception:
raise ValueError(self.message.format(value))

View file

@ -1,6 +1,25 @@
Cookbook
========
Calling a Django management command
-----------------------------------
.. versionadded:: 0.9
If you want to call a Django management command programmatically, say
from a script outside of your usual Django code, you can use the
equivalent of Django's :func:`~django.core.management.call_command` function
with django-configurations, too.
Simply import it from ``configurations.management`` instead:
.. code-block:: python
:emphasize-lines: 1
from configurations.management import call_command
call_command('dumpdata', exclude=['contenttypes', 'auth'])
Envdir
------
@ -14,7 +33,9 @@ Imagine for example you want to set a few environment variables, all you
have to do is to create a directory with files that have capitalized names
and contain the values you want to set.
Example::
Example:
.. code-block:: console
$ tree mysite_env/
mysite_env/
@ -30,7 +51,9 @@ Example::
$
Then, to enable the ``mysite_env`` environment variables, simply use the
``envdir`` command line tool as a prefix for your program, e.g.::
``envdir`` command line tool as a prefix for your program, e.g.:
.. code-block:: console
$ envdir mysite_env python manage.py runserver
@ -51,13 +74,17 @@ using pip_ to install packages.
Django 1.5.x
^^^^^^^^^^^^
First install Django 1.5.x and django-configurations::
First install Django 1.5.x and django-configurations:
pip install -r https://raw.github.com/jezdez/django-configurations/templates/1.5.x/requirements.txt
.. code-block:: console
Then create your new Django project with the provided template::
$ pip install -r https://raw.github.com/jezdez/django-configurations/templates/1.5.x/requirements.txt
django-admin.py startproject mysite -v2 --template https://github.com/jezdez/django-configurations/archive/templates/1.5.x.zip
Then create your new Django project with the provided template:
.. code-block:: console
$ django-admin.py startproject mysite -v2 --template https://github.com/jezdez/django-configurations/archive/templates/1.5.x.zip
See the repository of the template for more information:
@ -66,13 +93,17 @@ See the repository of the template for more information:
Django 1.6.x
^^^^^^^^^^^^
First install Django 1.6.x and django-configurations::
First install Django 1.6.x and django-configurations:
pip install -r https://raw.github.com/jezdez/django-configurations/templates/1.6.x/requirements.txt
.. code-block:: console
Or Django 1.6::
$ pip install -r https://raw.github.com/jezdez/django-configurations/templates/1.6.x/requirements.txt
django-admin.py startproject mysite -v2 --template https://github.com/jezdez/django-configurations/archive/templates/1.6.x.zip
Or Django 1.6:
.. code-block:: console
$ django-admin.py startproject mysite -v2 --template https://github.com/jezdez/django-configurations/archive/templates/1.6.x.zip
Now you have a default Django 1.5.x or 1.6.x project in the ``mysite``
directory that uses django-configurations.
@ -90,7 +121,9 @@ Celery
^^^^^
Given Celery's way to load Django settings in worker processes you should
probably just add the following to the **beginning** of your settings module::
probably just add the following to the **beginning** of your settings module:
.. code-block:: python
from configurations import importer
importer.install()
@ -145,26 +178,32 @@ enable an extension in your IPython configuration. See the IPython
documentation for how to create and `manage your IPython profile`_ correctly.
Here's a quick how-to in case you don't have a profile yet. Type in your
command line shell::
command line shell:
ipython profile create
.. code-block:: console
$ ipython profile create
Then let IPython show you where the configuration file ``ipython_config.py``
was created::
was created:
ipython locate profile
.. code-block:: console
$ ipython locate profile
That should print a directory path where you can find the
``ipython_config.py`` configuration file. Now open that file and extend the
``c.InteractiveShellApp.extensions`` configuration value. It may be commented
out from when IPython created the file or it may not exist in the file at all.
In either case make sure it's not a Python comment anymore and reads like this::
In either case make sure it's not a Python comment anymore and reads like this:
# A list of dotted module names of IPython extensions to load.
c.InteractiveShellApp.extensions = [
# .. your other extensions if available
'configurations',
]
.. code-block:: python
# A list of dotted module names of IPython extensions to load.
c.InteractiveShellApp.extensions = [
# .. your other extensions if available
'configurations',
]
That will tell IPython to load django-configurations correctly on startup.
It also works with django-extensions's shell_plus_ management command.
@ -179,12 +218,14 @@ FastCGI
In case you use FastCGI for deploying Django (you really shouldn't) and aren't
allowed to use Django's runfcgi_ management command (that would automatically
handle the setup for your if you've followed the quickstart guide above), make
sure to use something like the following script::
sure to use something like the following script:
.. code-block:: python
#!/usr/bin/env python
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'MySiteConfiguration')
@ -196,3 +237,33 @@ As you can see django-configurations provides a helper module
``configurations.fastcgi`` that handles the setup of your configurations.
.. _runfcgi: https://docs.djangoproject.com/en/1.5/howto/deployment/fastcgi/
Sphinx
------
.. versionadded: 0.9
In case you would like to user the amazing `autodoc` feature of the
documentation tool `Sphinx <http://sphinx-doc.org/>`_, you need add
django-configurations to your ``extensions`` config variable and set
the environment variable accordingly:
.. code-block:: python
:emphasize-lines: 2-3, 12
# My custom Django environment variables
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
# ...
'configurations',
]
# ...

View file

@ -1,11 +0,0 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings.main')
os.environ.setdefault('DJANGO_CONFIGURATION', 'Test')
from configurations.management import execute_from_command_line
execute_from_command_line(sys.argv)

View file

@ -1,2 +1,11 @@
[pytest]
django_find_project = false
addopts = --cov configurations
DJANGO_SETTINGS_MODULE = tests.settings.main
DJANGO_CONFIGURATION = Test
[wheel]
universal = 1
[flake8]
ignore = E124,E501,E127,E128

1
test_project/.env Normal file
View file

@ -0,0 +1 @@
DJANGO_DOTENV_VALUE='is set'

View file

@ -160,20 +160,6 @@ class Base(Configuration):
}
}
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',
'configurations',
)
class Debug(Base):
YEAH = True

View file

@ -0,0 +1,8 @@
from configurations import Configuration, values
class DotEnvConfiguration(Configuration):
DOTENV = 'test_project/.env'
DOTENV_VALUE = values.Value()

View file

@ -3,9 +3,13 @@ import uuid
import django
from configurations import Configuration, pristinemethod
from configurations.values import BooleanValue
class Test(Configuration):
ENV_LOADED = BooleanValue(False)
DEBUG = True
SITE_ID = 1
@ -30,9 +34,6 @@ class Test(Configuration):
ROOT_URLCONF = 'tests.urls'
if django.VERSION[:2] < (1, 6):
TEST_RUNNER = 'discover_runner.DiscoverRunner'
def TEMPLATE_CONTEXT_PROCESSORS(self):
return Configuration.TEMPLATE_CONTEXT_PROCESSORS + (
'tests.settings.base.test_callback',

14
tests/test_env.py Normal file
View file

@ -0,0 +1,14 @@
import os
from django.test import TestCase
from mock import patch
class DotEnvLoadingTests(TestCase):
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='DotEnvConfiguration',
DJANGO_SETTINGS_MODULE='tests.settings.dot_env')
def test_env_loaded(self):
from tests.settings import dot_env
self.assertEqual(dot_env.DOTENV_VALUE, 'is set')
self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV)

View file

@ -109,6 +109,12 @@ class ValueTests(TestCase):
with env(DJANGO_TEST='nonboolean'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_boolean_values_assign_false_to_another_booleanvalue(self):
value1 = BooleanValue(False)
value2 = BooleanValue(value1)
self.assertFalse(value1.setup('TEST1'))
self.assertFalse(value2.setup('TEST2'))
def test_integer_values(self):
value = IntegerValue(1)
with env(DJANGO_TEST='2'):
@ -352,6 +358,21 @@ class ValueTests(TestCase):
'USER': '',
}})
def test_database_url_additional_args(self):
def mock_database_url_caster(self, url, engine=None):
return { 'URL': url, 'ENGINE': engine }
with patch('configurations.values.DatabaseURLValue.caster', mock_database_url_caster):
value = DatabaseURLValue(engine='django_mysqlpool.backends.mysqlpool')
with env(DATABASE_URL='sqlite://'):
self.assertEqual(value.setup('DATABASE_URL'), {
'default': {
'URL': 'sqlite://',
'ENGINE': 'django_mysqlpool.backends.mysqlpool'
}
})
def test_email_url_value(self):
value = EmailURLValue()
self.assertEqual(value.default, {})

19
tox.ini
View file

@ -20,32 +20,27 @@ basepython =
pypy: pypy
usedevelop = true
deps =
django-discover-runner
mock
dj-database-url
dj-email-url
dj-search-url
django-cache-url>=0.6.0
six
https://github.com/pytest-dev/pytest-django/archive/master.zip#egg=pytest-django
pytest-cov
dj14: https://github.com/django/django/archive/stable/1.4.x.zip#egg=django
dj15: https://github.com/django/django/archive/stable/1.5.x.zip#egg=django
dj16: https://github.com/django/django/archive/stable/1.6.x.zip#egg=django
dj17: https://github.com/django/django/archive/stable/1.7.x.zip#egg=django
dj18: https://github.com/django/django/archive/master.zip#egg=django
flake8: flake8
coverage: coverage
commands =
python manage.py test -v2 {posargs:tests} --failfast
py.test {posargs:}
[testenv:flake8-py27]
commands = flake8 configurations --ignore=E501,E127,E128,E124
commands = flake8 configurations
deps = flake8
[testenv:flake8-py33]
commands = flake8 configurations --ignore=E501,E127,E128,E124
[testenv:coverage-py27-dj16]
commands = coverage erase
coverage run --source=. manage.py test -v2 {posargs:tests}
coverage report -m configurations/*py
coverage html configurations/*py
commands = flake8 configurations
deps = flake8