Compare commits

..

No commits in common. "master" and "0.2" have entirely different histories.
master ... 0.2

70 changed files with 1285 additions and 3850 deletions

View file

@ -1,13 +0,0 @@
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly

View file

@ -1,53 +0,0 @@
name: Release
on:
push:
tags:
- '*'
jobs:
build:
if: github.repository == 'jazzband/django-configurations'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Get pip cache dir
id: pip-cache
run: |
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: release-${{ hashFiles('**/setup.py') }}
restore-keys: |
release-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade setuptools twine wheel
- name: Build package
run: |
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository-url: https://jazzband.co/projects/django-configurations/upload

View file

@ -1,57 +0,0 @@
name: Test
on:
pull_request:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 6
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ matrix.python-version }}-v1-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade "tox<4" "tox-gh-actions<3"
- name: Tox tests
run: |
tox --verbose
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
name: coverage-data-${{ matrix.python-version }}
path: ".coverage.*"
include-hidden-files: true
merge-multiple: true
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

7
.gitignore vendored
View file

@ -1,12 +1,5 @@
.coverage
coverage.xml
docs/_build
*.egg-info
*.egg
test.db
build/
.tox/
htmlcov/
*.pyc
dist/
.eggs/

View file

@ -1 +0,0 @@
repos: []

View file

@ -1,16 +0,0 @@
---
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
python:
install:
- requirements: docs/requirements.txt
- method: pip
path: .
sphinx:
configuration: docs/conf.py
formats:
- epub
- pdf

28
.travis.yml Normal file
View file

@ -0,0 +1,28 @@
language: python
python:
- 2.6
- 2.7
- 3.2
- 3.3
before_install:
- export PIP_USE_MIRRORS=true
- export PIP_INDEX_URL=https://simple.crate.io/
install:
- pip install -e .
- pip install -r requirements/tests.txt Django==$DJANGO
script:
- make test
env:
- DJANGO=1.3.7
- DJANGO=1.4.5
- DJANGO=1.5
matrix:
exclude:
- python: 3.2
env: DJANGO=1.4.5
- python: 3.2
env: DJANGO=1.3.7
- python: 3.3
env: DJANGO=1.4.5
- python: 3.3
env: DJANGO=1.3.7

18
AUTHORS
View file

@ -1,22 +1,4 @@
Arseny Sokolov
Asif Saif Uddin
Baptiste Mispelon
Brian Helba
Bruno Clermont
Christoph Krybus
Finn-Thorben Sell
Gilles Fabio
Jannis Leidel
John Franey
Marc Abramowitz
Michael Käufl
Michael van Tellingen
Mike Fogel
Miro Hrončok
Nicholas Dujay
Paolo Melchiorre
Peter Bittner
Richard de Wit
Thomas Grainger
Tim Gates
Victor Seva

34
CHANGES.rst Normal file
View file

@ -0,0 +1,34 @@
.. :changelog:
Changelog
---------
v0.2 (2013-03-27)
^^^^^^^^^^^^^^^^^
- **backward incompatible change** Dropped support for Python 2.5! Please use
the 0.1 version if you really want.
- Added Python>3.2 and Django 1.5 support!
- Catch error when getting or evaluating callable setting class attributes.
- Simplified and extended tests.
- Added optional ``-C``/``--configuration`` management command option similar
to Django's ``--settings`` option
- Fixed the runserver message about which setting is used to
show the correct class.
- Stopped hiding AttributeErrors happening during initialization
of settings classes.
- Added FastCGI helper.
- Minor documentation fixes
v0.1 (2012-07-21)
^^^^^^^^^^^^^^^^^
- Initial public release

View file

@ -1,46 +0,0 @@
# Code of Conduct
As contributors and maintainers of the Jazzband projects, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in the Jazzband a harassment-free experience
for everyone, regardless of the level of experience, gender, gender identity and
expression, sexual orientation, disability, personal appearance, body size, race,
ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses,
without explicit permission
- Other unethical or unprofessional conduct
The Jazzband roadies have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, the roadies commit themselves to fairly and
consistently applying these principles to every aspect of managing the jazzband
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
removed from the Jazzband roadies.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/

View file

@ -1,3 +0,0 @@
[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/)
This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/docs/conduct) and follow the [guidelines](https://jazzband.co/docs/guidelines).

View file

@ -1,4 +1,4 @@
Copyright (c) 2012-2023, Jannis Leidel and other contributors.
Copyright (c) 2012, Jannis Leidel and other contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View file

@ -1,11 +1,7 @@
include .pre-commit-config.yaml
include .readthedocs.yaml
include AUTHORS
include CODE_OF_CONDUCT.md
include CONTRIBUTING.md
include LICENSE
include README.rst
include tox.ini
recursive-include docs *
recursive-include test_project *
recursive-include tests *
include CHANGES.rst
include .travis.yml
include manage.py
include Makefile
include requirements/tests.txt
recursive-include docs *

12
Makefile Normal file
View file

@ -0,0 +1,12 @@
.PHONY: test release doc
test:
flake8 configurations --ignore=E501,E127,E128,E124
coverage run --branch --source=configurations manage.py test configurations
coverage report --omit=configurations/test*
release:
python setup.py sdist register upload -s
doc:
cd docs; make html; cd ..

View file

@ -1,45 +1,15 @@
django-configurations |latest-version|
======================================
django-configurations
=====================
|jazzband| |build-status| |codecov| |docs| |python-support| |django-support|
.. image:: https://secure.travis-ci.org/jezdez/django-configurations.png
:alt: Build Status
:target: https://secure.travis-ci.org/jezdez/django-configurations
django-configurations eases Django project configuration by relying
on the composability of Python classes. It extends the notion of
Django's module based settings loading with well established
object oriented programming patterns.
Check out the `documentation`_ for more complete examples.
.. |latest-version| image:: https://img.shields.io/pypi/v/django-configurations.svg
:target: https://pypi.python.org/pypi/django-configurations
:alt: Latest version on PyPI
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg
:target: https://jazzband.co/
:alt: Jazzband
.. |build-status| image:: https://github.com/jazzband/django-configurations/workflows/Test/badge.svg
:target: https://github.com/jazzband/django-configurations/actions
:alt: Build Status
.. |codecov| image:: https://codecov.io/github/jazzband/django-configurations/coverage.svg?branch=master
:target: https://codecov.io/github/jazzband/django-configurations?branch=master
:alt: Test coverage status
.. |docs| image:: https://img.shields.io/readthedocs/django-configurations/latest.svg
:target: https://readthedocs.org/projects/django-configurations/
:alt: Documentation status
.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-configurations.svg
:target: https://pypi.python.org/pypi/django-configurations
:alt: Supported Python versions
.. |django-support| image:: https://img.shields.io/pypi/djversions/django-configurations
:target: https://pypi.org/project/django-configurations
:alt: Supported Django versions
.. _documentation: https://django-configurations.readthedocs.io/en/latest/
Quickstart
----------
@ -47,15 +17,9 @@ Install django-configurations:
.. code-block:: console
$ python -m pip install django-configurations
pip install django-configurations
or, alternatively, if you want to use URL-based values:
.. code-block:: console
$ python -m pip install django-configurations[cache,database,email,search]
Then subclass the included ``configurations.Configuration`` class in your
Then subclass the included ``configurations.Settings`` class in your
project's **settings.py** or any other module you're using to store the
settings constants, e.g.:
@ -63,9 +27,9 @@ settings constants, e.g.:
# mysite/settings.py
from configurations import Configuration
from configurations import Settings
class Dev(Configuration):
class MySiteSettings(Settings):
DEBUG = True
Set the ``DJANGO_CONFIGURATION`` environment variable to the name of the class
@ -73,25 +37,23 @@ you just created, e.g. in bash:
.. code-block:: console
$ export DJANGO_CONFIGURATION=Dev
export DJANGO_CONFIGURATION=MySiteSettings
and the ``DJANGO_SETTINGS_MODULE`` environment variable to the module
import path as usual, e.g. in bash:
.. code-block:: console
$ export DJANGO_SETTINGS_MODULE=mysite.settings
export DJANGO_SETTINGS_MODULE=mysite.settings
*Alternatively* supply the ``--configuration`` option when using Django
management commands along the lines of Django's default ``--settings``
command line option, e.g.
command line option, e.g.::
.. code-block:: console
$ python -m manage runserver --settings=mysite.settings --configuration=Dev
python manage.py runserver --settings=mysite.settings --configuration=MySiteSettings
To enable Django to use your configuration you now have to modify your
**manage.py**, **wsgi.py** or **asgi.py** script to use django-configurations's versions
**manage.py** or **wsgi.py** script to use django-configurations's versions
of the appropriate starter functions, e.g. a typical **manage.py** using
django-configurations would look like this:
@ -104,13 +66,13 @@ django-configurations would look like this:
if __name__ == "__main__":
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
os.environ.setdefault('DJANGO_CONFIGURATION', 'MySiteSettings')
from configurations.management import execute_from_command_line
execute_from_command_line(sys.argv)
Notice in line 10 we don't use the common tool
Notice in line 9 we don't use the common tool
``django.core.management.execute_from_command_line`` but instead
``configurations.management.execute_from_command_line``.
@ -121,7 +83,7 @@ The same applies to your **wsgi.py** file, e.g.:
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
os.environ.setdefault('DJANGO_CONFIGURATION', 'MySiteSettings')
from configurations.wsgi import get_wsgi_application
@ -130,18 +92,5 @@ The same applies to your **wsgi.py** file, e.g.:
Here we don't use the default ``django.core.wsgi.get_wsgi_application``
function but instead ``configurations.wsgi.get_wsgi_application``.
Or if you are not serving your app via WSGI but ASGI instead, you need to modify your **asgi.py** file too.:
.. code-block:: python
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
from configurations.asgi import get_asgi_application
application = get_asgi_application()
That's it! You can now use your project with ``manage.py`` and your favorite
WSGI/ASGI enabled server.
That's it! You can now use your project with **manage.py** and your favorite
WSGI enabled server.

View file

@ -1,31 +1,5 @@
from .base import Configuration # noqa
from .decorators import pristinemethod # noqa
from .version import __version__ # noqa
# flake8: noqa
from .base import Settings
__all__ = ['Configuration', 'pristinemethod']
def _setup():
from . import importer
importer.install()
from django.apps import apps
if not apps.ready:
import django
django.setup()
def load_ipython_extension(ipython):
"""
The `ipython` argument is the currently active `InteractiveShell`
instance, which can be used in any way. This allows you to register
new magics or aliases, for example.
"""
_setup()
def setup(app=None):
"""Function used to initialize configurations similar to :func:`.django.setup`."""
_setup()
__version__ = '0.2'
__all__ = ['Settings']

View file

@ -1,10 +0,0 @@
"""
invokes django-cadmin when the configurations module is run as a script.
Example: python -m configurations check
"""
from .management import execute_from_command_line
if __name__ == "__main__":
execute_from_command_line()

View file

@ -1,8 +0,0 @@
from . import importer
importer.install()
from django.core.asgi import get_asgi_application # noqa: E402
# this is just for the crazy ones
application = get_asgi_application()

View file

@ -1,86 +1,46 @@
import os
import re
import six
from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured
from .utils import uppercase_attributes
from .values import Value, setup_value
__all__ = ['Configuration']
__all__ = ['Settings']
install_failure = ("django-configurations settings importer wasn't "
"correctly installed. Please use one of the starter "
"functions to install it as mentioned in the docs: "
"https://django-configurations.readthedocs.io/")
"http://django-configurations.readthedocs.org/")
class ConfigurationBase(type):
class SettingsBase(type):
def __new__(cls, name, bases, attrs):
if bases not in ((object,), ()) and bases[0].__name__ != 'NewBase':
if bases != (object,) and bases[0].__name__ != 'NewBase':
# if this is actually a subclass in a settings module
# we better check if the importer was correctly installed
from . import importer
if not importer.installed:
raise ImproperlyConfigured(install_failure)
settings_vars = uppercase_attributes(global_settings)
parents = [base for base in bases if isinstance(base,
ConfigurationBase)]
parents = [base for base in bases if isinstance(base, SettingsBase)]
if parents:
for base in bases[::-1]:
settings_vars.update(uppercase_attributes(base))
deprecated_settings = {
# DEFAULT_HASHING_ALGORITHM is always deprecated, as it's a
# transitional setting
# https://docs.djangoproject.com/en/3.1/releases/3.1/#default-hashing-algorithm-settings
"DEFAULT_HASHING_ALGORITHM",
# DEFAULT_CONTENT_TYPE and FILE_CHARSET are deprecated in
# Django 2.2 and are removed in Django 3.0
"DEFAULT_CONTENT_TYPE",
"FILE_CHARSET",
# When DEFAULT_AUTO_FIELD is not explicitly set, Django's emits a
# system check warning models.W042. This warning should not be
# suppressed, as downstream users are expected to make a decision.
# https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys
"DEFAULT_AUTO_FIELD",
# FORMS_URLFIELD_ASSUME_HTTPS is a transitional setting introduced
# in Django 5.0.
# https://docs.djangoproject.com/en/5.0/releases/5.0/#id2
"FORMS_URLFIELD_ASSUME_HTTPS"
}
# PASSWORD_RESET_TIMEOUT_DAYS is deprecated in favor of
# PASSWORD_RESET_TIMEOUT in Django 3.1
# https://github.com/django/django/commit/226ebb17290b604ef29e82fb5c1fbac3594ac163#diff-ec2bed07bb264cb95a80f08d71a47c06R163-R170
if "PASSWORD_RESET_TIMEOUT" in settings_vars:
deprecated_settings.add("PASSWORD_RESET_TIMEOUT_DAYS")
# DEFAULT_FILE_STORAGE and STATICFILES_STORAGE are deprecated
# in favor of STORAGES.
# https://docs.djangoproject.com/en/dev/releases/4.2/#custom-file-storages
if "STORAGES" in settings_vars:
deprecated_settings.add("DEFAULT_FILE_STORAGE")
deprecated_settings.add("STATICFILES_STORAGE")
for deprecated_setting in deprecated_settings:
if deprecated_setting in settings_vars:
del settings_vars[deprecated_setting]
attrs = {**settings_vars, **attrs}
return super().__new__(cls, name, bases, attrs)
attrs = dict(settings_vars, **attrs)
return super(SettingsBase, cls).__new__(cls, name, bases, attrs)
def __repr__(self):
return "<Configuration '{}.{}'>".format(self.__module__,
self.__name__)
return "<Settings '%s.%s'>" % (self.__module__, self.__name__)
class Configuration(metaclass=ConfigurationBase):
class Settings(six.with_metaclass(SettingsBase)):
"""
The base configuration class to inherit from.
::
class Develop(Configuration):
class Develop(Settings):
EXTRA_AWESOME = True
@property
@ -98,60 +58,4 @@ class Configuration(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.
https://wellfire.co/learn/easier-12-factor-django/
https://gist.github.com/bennylope/2999704
"""
# check if the class has DOTENV set whether 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) as f:
content = f.read()
except OSError as e:
raise ImproperlyConfigured("Couldn't read .env file "
"with the path {}. Error: "
"{}".format(dotenv, e)) from 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):
if cls.DOTENV_LOADED is None:
cls.load_dotenv()
@classmethod
def post_setup(cls):
pass
@classmethod
def setup(cls):
for name, value in uppercase_attributes(cls).items():
if isinstance(value, Value):
setup_value(cls, name, value)
pass

View file

@ -1,19 +0,0 @@
def pristinemethod(func):
"""
A decorator for handling pristine settings like callables.
Use it like this::
from configurations import Configuration, pristinemethod
class Develop(Configuration):
@pristinemethod
def USER_CHECK(user):
return user.check_perms()
GROUP_CHECK = pristinemethod(lambda user: user.has_group_access())
"""
func.pristine = True
return staticmethod(func)

View file

@ -1,180 +1,154 @@
from importlib.machinery import PathFinder
import logging
import imp
import os
import sys
from optparse import OptionParser, make_option
from functools import wraps
from optparse import make_option, OptionParser
from django.conf import ENVIRONMENT_VARIABLE as SETTINGS_ENVIRONMENT_VARIABLE
import django
from django.core.exceptions import ImproperlyConfigured
from django.core.management import base
from django.conf import ENVIRONMENT_VARIABLE
from django.utils.decorators import available_attrs
from django.utils.importlib import import_module
from .utils import uppercase_attributes
from .utils import uppercase_attributes, reraise
from .values import Value, setup_value
installed = False
CONFIGURATION_ENVIRONMENT_VARIABLE = 'DJANGO_CONFIGURATION'
CONFIGURATION_ARGUMENT = '--configuration'
CONFIGURATION_ARGUMENT_HELP = ('The name of the configuration class to load, '
'e.g. "Development". If this isn\'t provided, '
'the DJANGO_CONFIGURATION environment '
'variable will be used.')
class StdoutWrapper(object):
"""
Wrap the stdout to patch one line, yup.
"""
def __init__(self, out):
self._out = out
def __getattr__(self, name):
return getattr(self._out, name)
def write(self, msg, *args, **kwargs):
if msg.startswith('Django version'):
from django.conf import settings
msg_parts = msg.split('\n')
msg_parts[0] = ("Django version %s, using settings %r" %
(django.get_version(), settings.CONFIGURATION))
msg = '\n'.join(msg_parts)
return self._out.write(msg, *args, **kwargs)
configuration_options = (make_option(CONFIGURATION_ARGUMENT,
help=CONFIGURATION_ARGUMENT_HELP),)
def patch_inner_run(original):
@wraps(original, assigned=available_attrs(original))
def inner_run(self, *args, **options):
if hasattr(self, 'stdout'):
self.stdout = StdoutWrapper(self.stdout)
return original(self, *args, **options)
return inner_run
configuration_options = (
make_option('--configuration',
help='The name of the settings class to load, e.g. '
'"Development". If this isn\'t provided, the '
'DJANGO_CONFIGURATION environment variable will '
'be used.'),)
def install(check_options=False):
def install():
global installed
if not installed:
orig_create_parser = base.BaseCommand.create_parser
def create_parser(self, prog_name, subcommand):
parser = orig_create_parser(self, prog_name, subcommand)
if isinstance(parser, OptionParser):
# in case the option_list is set the create_parser
# will actually return a OptionParser for backward
# compatibility. In that case we should tack our
# options on to the end of the parser on the way out.
for option in configuration_options:
parser.add_option(option)
else:
# probably argparse, let's not import argparse though
parser.add_argument(CONFIGURATION_ARGUMENT,
help=CONFIGURATION_ARGUMENT_HELP)
return parser
from django.core import management
from django.core.management import base
base.BaseCommand.create_parser = create_parser
importer = ConfigurationFinder(check_options=check_options)
sys.meta_path.insert(0, importer)
# add the configuration option to all management commands
base.BaseCommand.option_list += configuration_options
sys.meta_path.insert(0, SettingsImporter())
installed = True
# now patch the active runserver command to show a nicer output
commands = management.get_commands()
runserver_path = commands.get('runserver', None)
if runserver_path is not None:
full_path = '%s.management.commands.runserver' % runserver_path
try:
runserver_module = import_module(full_path)
except ImportError:
pass
else:
original_inner_run = runserver_module.Command.inner_run
runserver_module.Command.inner_run = patch_inner_run(original_inner_run)
class ConfigurationFinder(PathFinder):
modvar = SETTINGS_ENVIRONMENT_VARIABLE
namevar = CONFIGURATION_ENVIRONMENT_VARIABLE
error_msg = ("Configuration cannot be imported, "
"environment variable {0} is undefined.")
def __init__(self, check_options=False):
self.argv = sys.argv[:]
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
self.logger.addHandler(handler)
if check_options:
self.check_options()
class SettingsImporter(object):
class_varname = 'DJANGO_CONFIGURATION'
error_msg = "Settings cannot be imported, environment variable %s is undefined."
def __init__(self):
parser = OptionParser(option_list=configuration_options, add_help_option=False)
options, args = parser.parse_args(sys.argv[2:])
if options.configuration:
os.environ[self.class_varname] = options.configuration
self.validate()
if check_options:
self.announce()
def __repr__(self):
return "<ConfigurationFinder for '{}.{}'>".format(self.module,
self.name)
return "<SettingsImporter for '%s.%s'>" % (self.module, self.name)
@property
def module(self):
return os.environ.get(self.modvar)
return os.environ.get(ENVIRONMENT_VARIABLE)
@property
def name(self):
return os.environ.get(self.namevar)
def check_options(self):
parser = base.CommandParser(
usage="%(prog)s subcommand [options] [args]",
add_help=False,
)
parser.add_argument('--settings')
parser.add_argument('--pythonpath')
parser.add_argument(CONFIGURATION_ARGUMENT,
help=CONFIGURATION_ARGUMENT_HELP)
parser.add_argument('args', nargs='*') # catch-all
try:
options, args = parser.parse_known_args(self.argv[2:])
if options.configuration:
os.environ[self.namevar] = options.configuration
base.handle_default_options(options)
except base.CommandError:
pass # Ignore any option errors at this point.
return os.environ.get(self.class_varname)
def validate(self):
if self.name is None:
raise ImproperlyConfigured(self.error_msg.format(self.namevar))
raise ImproperlyConfigured(self.error_msg % self.class_varname)
if self.module is None:
raise ImproperlyConfigured(self.error_msg.format(self.modvar))
raise ImproperlyConfigured(self.error_msg % ENVIRONMENT_VARIABLE)
def announce(self):
if len(self.argv) > 1:
from . import __version__
from django.utils.termcolors import colorize
from django.core.management.color import no_style
if '--no-color' in self.argv:
stylize = no_style()
else:
def stylize(text):
return colorize(text, fg='green')
if (self.argv[1] == 'runserver'
and os.environ.get('RUN_MAIN') == 'true'):
message = ("django-configurations version {}, using "
"configuration {}".format(__version__ or "",
self.name))
self.logger.debug(stylize(message))
def find_spec(self, fullname, path=None, target=None):
def find_module(self, fullname, path=None):
if fullname is not None and fullname == self.module:
spec = super().find_spec(fullname, path, target)
if spec is not None:
wrap_loader(spec.loader, self.name)
return spec
module = fullname.rsplit('.', 1)[-1]
return SettingsLoader(self.name, imp.find_module(module, path))
return None
class SettingsLoader(object):
def __init__(self, name, location):
self.name = name
self.location = location
def load_module(self, fullname):
if fullname in sys.modules:
mod = sys.modules[fullname] # pragma: no cover
else:
return None
def wrap_loader(loader, class_name):
class ConfigurationLoader(loader.__class__):
def exec_module(self, module):
super().exec_module(module)
mod = module
cls_path = f'{mod.__name__}.{class_name}'
try:
cls = getattr(mod, class_name)
except AttributeError as err: # pragma: no cover
reraise(
err,
(
f"Couldn't find configuration '{class_name}' in "
f"module '{mod.__package__}'"
),
)
try:
cls.pre_setup()
cls.setup()
obj = cls()
attributes = uppercase_attributes(obj).items()
for name, value in attributes:
if callable(value) and not getattr(value, 'pristine', False):
value = value()
# in case a method returns a Value instance we have
# to do the same as the Configuration.setup method
if isinstance(value, Value):
setup_value(mod, name, value)
continue
setattr(mod, name, value)
setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(module.__name__,
class_name))
cls.post_setup()
except Exception as err:
reraise(err, f"Couldn't setup configuration '{cls_path}'")
loader.__class__ = ConfigurationLoader
mod = imp.load_module(fullname, *self.location)
try:
cls = getattr(mod, self.name)
except AttributeError: # pragma: no cover
raise ImproperlyConfigured("Couldn't find settings '%s' in "
"module '%s'" %
(self.name, mod.__package__))
try:
obj = cls()
except Exception as err:
raise ImproperlyConfigured("Couldn't load settings '%s.%s': %s" %
(mod.__name__, self.name, err))
try:
attributes = uppercase_attributes(obj).items()
except Exception as err:
raise ImproperlyConfigured("Couldn't get items of settings '%s.%s': %s" %
(mod.__name__, self.name, err))
for name, value in attributes:
if callable(value):
try:
value = value()
except Exception as err:
raise ImproperlyConfigured(
"Couldn't execute callable '%s' in '%s.%s': %s" %
(value, mod.__name__, self.name, err))
setattr(mod, name, value)
setattr(mod, 'CONFIGURATION', '%s.%s' % (fullname, self.name))
return mod

View file

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

View file

@ -1,12 +0,0 @@
from . import _setup, __version__
def setup(app=None):
"""
The callback for Sphinx that acts as a Sphinx extension.
Add ``'configurations'`` to the ``extensions`` config variable
in your docs' ``conf.py``.
"""
_setup()
return {'version': __version__, 'parallel_read_safe': True}

View file

@ -0,0 +1,9 @@
from configurations import Settings
def test_callback(request):
return {}
class Base(Settings):
pass

View file

@ -0,0 +1,46 @@
import os
import uuid
from configurations import Settings
class Test(Settings):
SITE_ID = 1
SECRET_KEY = str(uuid.uuid4())
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(os.path.dirname(__file__), 'test.db'),
}
}
INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.auth',
'django.contrib.admin',
'configurations.tests',
]
ROOT_URLCONF = 'configurations.tests.urls'
TEST_RUNNER = 'discover_runner.DiscoverRunner'
TEST_SETTING = True
_SOMETHING = 'YEAH'
DEBUG = True
@property
def LALA(self):
return 1
def LALA2(self):
return 1
def TEMPLATE_CONTEXT_PROCESSORS(self):
return Settings.TEMPLATE_CONTEXT_PROCESSORS + (
'configurations.tests.settings.base.test_callback',)

View file

@ -0,0 +1,25 @@
from configurations import Settings
class Mixin1(object):
@property
def TEMPLATE_CONTEXT_PROCESSORS(self):
return super(Mixin1, self).TEMPLATE_CONTEXT_PROCESSORS + (
'some_app.context_processors.processor1',)
class Mixin2(object):
@property
def TEMPLATE_CONTEXT_PROCESSORS(self):
return super(Mixin2, self).TEMPLATE_CONTEXT_PROCESSORS + (
'some_app.context_processors.processor2',)
class Inheritance(Mixin2, Mixin1, Settings):
@property
def TEMPLATE_CONTEXT_PROCESSORS(self):
return super(Inheritance, self).TEMPLATE_CONTEXT_PROCESSORS + (
'some_app.context_processors.processorbase',)

View file

@ -0,0 +1,8 @@
from .main import Test
class Inheritance(Test):
def TEMPLATE_CONTEXT_PROCESSORS(self):
return super(Inheritance, self).TEMPLATE_CONTEXT_PROCESSORS() + (
'configurations.tests.settings.base.test_callback',)

View file

@ -0,0 +1,8 @@
from .base import Base
class Inheritance(Base):
def TEMPLATE_CONTEXT_PROCESSORS(self):
return super(Inheritance, self).TEMPLATE_CONTEXT_PROCESSORS + (
'configurations.tests.settings.base.test_callback',)

View file

@ -0,0 +1,42 @@
import os
from django.conf import global_settings
from django.test import TestCase
from mock import patch
class InheritanceTests(TestCase):
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='Inheritance',
DJANGO_SETTINGS_MODULE='configurations.tests.settings.single_inheritance')
def test_inherited(self):
from configurations.tests.settings import single_inheritance
self.assertEquals(single_inheritance.TEMPLATE_CONTEXT_PROCESSORS,
global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'configurations.tests.settings.base.test_callback',
))
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='Inheritance',
DJANGO_SETTINGS_MODULE='configurations.tests.settings.multiple_inheritance')
def test_inherited2(self):
from configurations.tests.settings import multiple_inheritance
self.assertEquals(multiple_inheritance.TEMPLATE_CONTEXT_PROCESSORS,
global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'configurations.tests.settings.base.test_callback',
'configurations.tests.settings.base.test_callback',
))
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='Inheritance',
DJANGO_SETTINGS_MODULE='configurations.tests.settings.mixin_inheritance')
def test_inherited3(self):
from configurations.tests.settings import mixin_inheritance
self.assertEquals(mixin_inheritance.TEMPLATE_CONTEXT_PROCESSORS,
global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'some_app.context_processors.processor1',
'some_app.context_processors.processor2',
'some_app.context_processors.processorbase',
))

View file

@ -0,0 +1,65 @@
import os
from django.conf import global_settings
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from mock import patch
from configurations.importer import SettingsImporter
class MainTests(TestCase):
def test_simple(self):
from configurations.tests.settings import main
self.assertEquals(main.LALA, 1)
self.assertEquals(main.TEMPLATE_CONTEXT_PROCESSORS,
global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'configurations.tests.settings.base.test_callback',
))
self.assertEquals(main.TEST_SETTING, True)
def test_global_arrival(self):
from django.conf import settings
self.assertEquals(settings.LALA, 1)
self.assertRaises(AttributeError, lambda: settings._SOMETHING)
@patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test')
def test_empty_module_var(self):
self.assertRaises(ImproperlyConfigured, SettingsImporter)
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='configurations.tests.settings.main')
def test_empty_class_var(self):
self.assertRaises(ImproperlyConfigured, SettingsImporter)
def test_global_settings(self):
from configurations.base import Settings
self.assertEquals(Settings.LOGGING_CONFIG, 'django.utils.log.dictConfig')
self.assertEquals(repr(Settings),
"<Settings 'configurations.base.Settings'>")
def test_repr(self):
from configurations.tests.settings.main import Test
self.assertEquals(repr(Test),
"<Settings 'configurations.tests.settings.main.Test'>")
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='configurations.tests.settings.main',
DJANGO_CONFIGURATION='Test')
def test_initialization(self):
importer = SettingsImporter()
self.assertEquals(importer.module, 'configurations.tests.settings.main')
self.assertEquals(importer.name, 'Test')
self.assertEquals(repr(importer),
"<SettingsImporter for 'configurations.tests.settings.main.Test'>")
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='configurations.tests.settings.inheritance',
DJANGO_CONFIGURATION='Inheritance')
def test_initialization_inheritance(self):
importer = SettingsImporter()
self.assertEquals(importer.module,
'configurations.tests.settings.inheritance')
self.assertEquals(importer.name, 'Inheritance')

View file

@ -0,0 +1,9 @@
from django.conf.urls.defaults import include, patterns
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
(r'^admin/', include(admin.site.urls)),
)

View file

@ -1,101 +1,7 @@
import inspect
import sys
import warnings
from functools import partial
from importlib import import_module
from django.core.exceptions import ImproperlyConfigured
def isuppercase(name):
return name == name.upper() and not name.startswith('_')
def uppercase_attributes(obj):
return {name: getattr(obj, name) for name in dir(obj) if isuppercase(name)}
def import_by_path(dotted_path, error_prefix=''):
"""
Import a dotted module path and return the attribute/class designated by
the last name in the path. Raise ImproperlyConfigured if something goes
wrong.
Backported from Django 1.6.
"""
warnings.warn("Function utils.import_by_path is deprecated in favor of "
"django.utils.module_loading.import_string.", DeprecationWarning)
try:
module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError:
raise ImproperlyConfigured("{}{} doesn't look like "
"a module path".format(error_prefix,
dotted_path))
try:
module = import_module(module_path)
except ImportError as err:
msg = '{}Error importing module {}: "{}"'.format(error_prefix,
module_path,
err)
raise ImproperlyConfigured(msg).with_traceback(sys.exc_info()[2])
try:
attr = getattr(module, class_name)
except AttributeError:
raise ImproperlyConfigured('{}Module "{}" does not define a '
'"{}" attribute/class'.format(error_prefix,
module_path,
class_name))
return attr
def reraise(exc, prefix=None, suffix=None):
args = exc.args
if not args:
args = ('',)
if prefix is None:
prefix = ''
elif not prefix.endswith((':', ': ')):
prefix = prefix + ': '
if suffix is None:
suffix = ''
elif not (suffix.startswith('(') and suffix.endswith(')')):
suffix = '(' + suffix + ')'
exc.args = (f'{prefix} {args[0]} {suffix}',) + args[1:]
raise exc
# Copied over from Sphinx
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)
return dict((name, getattr(obj, name))
for name in filter(isuppercase, dir(obj)))

View file

@ -1,474 +0,0 @@
import ast
import copy
import decimal
import os
import sys
from django.core import validators
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.module_loading import import_string
from .utils import getargspec
def setup_value(target, name, value):
actual_value = value.setup(name)
# overwriting the original Value class with the result
setattr(target, name, value.value)
if value.multiple:
for multiple_name, multiple_value in actual_value.items():
setattr(target, multiple_name, multiple_value)
class Value:
"""
A single settings value that is able to interpret env variables
and implements a simple validation scheme.
"""
multiple = False
late_binding = False
environ_required = False
@property
def value(self):
value = self.default
if not hasattr(self, '_value') and self.environ_name:
self.setup(self.environ_name)
if hasattr(self, '_value'):
value = self._value
return value
@value.setter
def value(self, value):
self._value = value
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.
"""
instance = object.__new__(cls)
if 'late_binding' in kwargs:
instance.late_binding = kwargs.get('late_binding')
if not instance.late_binding:
instance.__init__(*args, **kwargs)
if ((instance.environ and instance.environ_name)
or (not instance.environ and instance.default)):
instance = instance.setup(instance.environ_name)
return instance
def __init__(self, default=None, environ=True, environ_name=None,
environ_prefix='DJANGO', environ_required=False,
*args, **kwargs):
if isinstance(default, Value) and default.default is not None:
self.default = copy.copy(default.default)
else:
self.default = default
self.environ = environ
if environ_prefix and environ_prefix.endswith('_'):
environ_prefix = environ_prefix[:-1]
self.environ_prefix = environ_prefix
self.environ_name = environ_name
self.environ_required = environ_required
def __str__(self):
return str(self.value)
def __repr__(self):
return repr(self.value)
def __eq__(self, other):
return self.value == other
def __bool__(self):
return bool(self.value)
# Compatibility with python 2
__nonzero__ = __bool__
def full_environ_name(self, name):
if self.environ_name:
environ_name = self.environ_name
else:
environ_name = name.upper()
if self.environ_prefix:
environ_name = f'{self.environ_prefix}_{environ_name}'
return environ_name
def setup(self, name):
value = self.default
if self.environ:
full_environ_name = self.full_environ_name(name)
if full_environ_name in os.environ:
value = self.to_python(os.environ[full_environ_name])
elif self.environ_required:
raise ValueError('Value {!r} is required to be set as the '
'environment variable {!r}'
.format(name, full_environ_name))
self.value = value
return value
def to_python(self, value):
"""
Convert the given value of a environment variable into an
appropriate Python representation of the value.
This should be overridden when subclassing.
"""
return value
class MultipleMixin:
multiple = True
class BooleanValue(Value):
true_values = ('yes', 'y', 'true', '1')
false_values = ('no', 'n', 'false', '0', '')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.default not in (True, False):
raise ValueError('Default value {!r} is not a '
'boolean value'.format(self.default))
def to_python(self, value):
normalized_value = value.strip().lower()
if normalized_value in self.true_values:
return True
elif normalized_value in self.false_values:
return False
else:
raise ValueError('Cannot interpret '
'boolean value {!r}'.format(value))
class CastingMixin:
exception = (TypeError, ValueError)
message = 'Cannot interpret value {0!r}'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if isinstance(self.caster, str):
try:
self._caster = import_string(self.caster)
except ImportError as err:
msg = f"Could not import {self.caster!r}"
raise ImproperlyConfigured(msg) from err
elif callable(self.caster):
self._caster = self.caster
else:
error = 'Cannot use caster of {} ({!r})'.format(self,
self.caster)
raise ValueError(error)
try:
arg_names = getargspec(self._caster)[0]
self._params = {name: kwargs[name] for name in arg_names if name in kwargs}
except TypeError:
self._params = {}
def to_python(self, value):
try:
if self._params:
return self._caster(value, **self._params)
else:
return self._caster(value)
except self.exception:
raise ValueError(self.message.format(value))
class IntegerValue(CastingMixin, Value):
caster = int
class PositiveIntegerValue(IntegerValue):
def to_python(self, value):
int_value = super().to_python(value)
if int_value < 0:
raise ValueError(self.message.format(value))
return int_value
class FloatValue(CastingMixin, Value):
caster = float
class DecimalValue(CastingMixin, Value):
caster = decimal.Decimal
exception = decimal.InvalidOperation
class SequenceValue(Value):
"""
Common code for sequence-type values (lists and tuples).
Do not use this class directly. Instead use a subclass.
"""
# Specify this value in subclasses, e.g. with 'list' or 'tuple'
sequence_type = None
converter = None
def __init__(self, *args, **kwargs):
msg = 'Cannot interpret {0} item {{0!r}} in {0} {{1!r}}'
self.message = msg.format(self.sequence_type.__name__)
self.separator = kwargs.pop('separator', ',')
converter = kwargs.pop('converter', None)
if converter is not None:
self.converter = converter
super().__init__(*args, **kwargs)
# make sure the default is the correct sequence type
if self.default is None:
self.default = self.sequence_type()
else:
self.default = self.sequence_type(self.default)
# initial conversion
if self.converter is not None:
self.default = self._convert(self.default)
def _convert(self, sequence):
converted_values = []
for value in sequence:
try:
converted_values.append(self.converter(value))
except (TypeError, ValueError):
raise ValueError(self.message.format(value, value))
return self.sequence_type(converted_values)
def to_python(self, value):
split_value = [v.strip() for v in value.strip().split(self.separator)]
# removing empty items
value_list = self.sequence_type(filter(None, split_value))
if self.converter is not None:
value_list = self._convert(value_list)
return self.sequence_type(value_list)
class ListValue(SequenceValue):
sequence_type = list
class TupleValue(SequenceValue):
sequence_type = tuple
class SingleNestedSequenceValue(SequenceValue):
"""
Common code for nested sequences (list of lists, or tuple of tuples).
Do not use this class directly. Instead use a subclass.
"""
def __init__(self, *args, **kwargs):
self.seq_separator = kwargs.pop('seq_separator', ';')
super().__init__(*args, **kwargs)
def _convert(self, items):
# This could receive either a bare or nested sequence
if items and isinstance(items[0], self.sequence_type):
converted_sequences = [
super(SingleNestedSequenceValue, self)._convert(i) for i in items
]
return self.sequence_type(converted_sequences)
return self.sequence_type(super()._convert(items))
def to_python(self, value):
split_value = [
v.strip() for v in value.strip().split(self.seq_separator)
]
# Remove empty items
filtered = self.sequence_type(filter(None, split_value))
sequence = [
super(SingleNestedSequenceValue, self).to_python(f) for f in filtered
]
return self.sequence_type(sequence)
class SingleNestedListValue(SingleNestedSequenceValue):
sequence_type = list
class SingleNestedTupleValue(SingleNestedSequenceValue):
sequence_type = tuple
class BackendsValue(ListValue):
def converter(self, value):
try:
import_string(value)
except ImportError as err:
raise ValueError(err).with_traceback(sys.exc_info()[2])
return value
class SetValue(ListValue):
message = 'Cannot interpret set item {0!r} in set {1!r}'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.default is None:
self.default = set()
else:
self.default = set(self.default)
def to_python(self, value):
return set(super().to_python(value))
class DictValue(Value):
message = 'Cannot interpret dict value {0!r}'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.default is None:
self.default = {}
else:
self.default = dict(self.default)
def to_python(self, value):
value = super().to_python(value)
if not value:
return {}
try:
evaled_value = ast.literal_eval(value)
except ValueError:
raise ValueError(self.message.format(value))
if not isinstance(evaled_value, dict):
raise ValueError(self.message.format(value))
return evaled_value
class ValidationMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if isinstance(self.validator, str):
try:
self._validator = import_string(self.validator)
except ImportError as err:
msg = f"Could not import {self.validator!r}"
raise ImproperlyConfigured(msg) from err
elif callable(self.validator):
self._validator = self.validator
else:
raise ValueError('Cannot use validator of '
'{} ({!r})'.format(self, self.validator))
if self.default:
self.to_python(self.default)
def to_python(self, value):
try:
self._validator(value)
except ValidationError:
raise ValueError(self.message.format(value))
else:
return value
class EmailValue(ValidationMixin, Value):
message = 'Cannot interpret email value {0!r}'
validator = 'django.core.validators.validate_email'
class URLValue(ValidationMixin, Value):
message = 'Cannot interpret URL value {0!r}'
validator = validators.URLValidator()
class IPValue(ValidationMixin, Value):
message = 'Cannot interpret IP value {0!r}'
validator = 'django.core.validators.validate_ipv46_address'
class RegexValue(ValidationMixin, Value):
message = "Regex doesn't match value {0!r}"
def __init__(self, *args, **kwargs):
regex = kwargs.pop('regex', None)
self.validator = validators.RegexValidator(regex=regex)
super().__init__(*args, **kwargs)
class PathValue(Value):
def __init__(self, *args, **kwargs):
self.check_exists = kwargs.pop('check_exists', True)
super().__init__(*args, **kwargs)
def setup(self, name):
value = super().setup(name)
value = os.path.expanduser(value)
if self.check_exists and not os.path.exists(value):
raise ValueError(f'Path {value!r} does not exist.')
return os.path.abspath(value)
class SecretValue(Value):
def __init__(self, *args, **kwargs):
kwargs['environ'] = True
kwargs['environ_required'] = True
super().__init__(*args, **kwargs)
if self.default is not None:
raise ValueError('Secret values are only allowed to '
'be set as environment variables')
def setup(self, name):
value = super().setup(name)
if not value:
raise ValueError(f'Secret value {name!r} is not set')
return value
class EmailURLValue(CastingMixin, MultipleMixin, Value):
caster = 'dj_email_url.parse'
message = 'Cannot interpret email URL value {0!r}'
late_binding = True
def __init__(self, *args, **kwargs):
kwargs.setdefault('environ', True)
kwargs.setdefault('environ_prefix', None)
kwargs.setdefault('environ_name', 'EMAIL_URL')
super().__init__(*args, **kwargs)
if self.default is None:
self.default = {}
else:
self.default = self.to_python(self.default)
class DictBackendMixin(Value):
default_alias = 'default'
def __init__(self, *args, **kwargs):
self.alias = kwargs.pop('alias', self.default_alias)
kwargs.setdefault('environ', True)
kwargs.setdefault('environ_prefix', None)
kwargs.setdefault('environ_name', self.environ_name)
super().__init__(*args, **kwargs)
if self.default is None:
self.default = {}
else:
self.default = self.to_python(self.default)
def to_python(self, value):
value = super().to_python(value)
return {self.alias: value}
class DatabaseURLValue(DictBackendMixin, CastingMixin, Value):
caster = 'dj_database_url.parse'
message = 'Cannot interpret database URL value {0!r}'
environ_name = 'DATABASE_URL'
late_binding = True
class CacheURLValue(DictBackendMixin, CastingMixin, Value):
caster = 'django_cache_url.parse'
message = 'Cannot interpret cache URL value {0!r}'
environ_name = 'CACHE_URL'
late_binding = True
class SearchURLValue(DictBackendMixin, CastingMixin, Value):
caster = 'dj_search_url.parse'
message = 'Cannot interpret Search URL value {0!r}'
environ_name = 'SEARCH_URL'
late_binding = True

View file

@ -1,7 +0,0 @@
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("django-configurations")
except PackageNotFoundError:
# package is not installed
__version__ = None

View file

@ -2,7 +2,13 @@ from . import importer
importer.install()
from django.core.wsgi import get_wsgi_application # noqa: E402
try:
from django.core.wsgi import get_wsgi_application
except ImportError: # pragma: no cover
from django.core.handlers.wsgi import WSGIHandler
def get_wsgi_application(): # noqa
return WSGIHandler()
# this is just for the crazy ones
application = get_wsgi_application()

153
docs/Makefile Normal file
View file

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-configurations.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-configurations.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-configurations"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-configurations"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View file

@ -1,292 +0,0 @@
.. :changelog:
Changelog
---------
Unreleased
^^^^^^^^^^
- Prevent warning about ``FORMS_URLFIELD_ASSUME_HTTPS`` on Django 5.0.
v2.5.1 (2023-11-30)
^^^^^^^^^^^^^^^^^^^
- Add compatibility with Python 3.12
v2.5 (2023-10-20)
^^^^^^^^^^^^^^^^^
- Update Github actions and fix pipeline warnings
- Add compatibility with Django 5.0
- **BACKWARD INCOMPATIBLE** Drop compatibility for Django 4.0
- **BACKWARD INCOMPATIBLE** Drop compatibility for Python 3.7 and PyPy < 3.10
v2.4.2 (2023-09-27)
^^^^^^^^^^^^^^^^^^^
- Replace imp (due for removal in Python 3.12) with importlib
- Test on PyPy 3.10.
v2.4.1 (2023-04-04)
^^^^^^^^^^^^^^^^^^^
- Use furo as documentation theme
- Add compatibility with Django 4.2 - fix "STATICFILES_STORAGE/STORAGES are mutually exclusive" error.
- Test Django 4.1.3+ on Python 3.11
v2.4 (2022-08-24)
^^^^^^^^^^^^^^^^^
- Add compatibility with Django 4.1
- **BACKWARD INCOMPATIBLE** Drop compatibility for Django < 3.2
- **BACKWARD INCOMPATIBLE** Drop compatibility for Python 3.6
v2.3.2 (2022-01-25)
^^^^^^^^^^^^^^^^^^^
- Add compatibility with Django 4.0
- Fix regression where settings receiving a default were ignored. #323 #327
v2.3.1 (2021-11-08)
^^^^^^^^^^^^^^^^^^^
- Test Django 3.2 on Python 3.10 as well.
- Test on PyPy 3.6, 3.7 and 3.8.
- Enforce Python version requirement during installation (>=3.6).
- Fix and refactor the documentation build process.
v2.3 (2021-10-27)
^^^^^^^^^^^^^^^^^
- **BACKWARD INCOMPATIBLE** Drop support for Python 2.7 and 3.5.
- **BACKWARD INCOMPATIBLE** Drop support for Django < 2.2.
- Add support for Django 3.1 and 3.2.
- Add suppport for Python 3.9 and 3.10.
- Deprecate ``utils.import_by_path`` in favor of
``django.utils.module_loading.import_string``.
- Add ASGI support.
- Added "python -m configurations" entry point.
- Make package ``install_requires`` include ``django>=2.2``.
- Prevent an ImproperlyConfigured warning from ``DEFAULT_HASHING_ALGORITHM``.
- Prevent warnings for settings deprecated in Django 2.2
(``DEFAULT_CONTENT_TYPE`` and ``FILE_CHARSET``).
- Preserve Django warnings when ``DEFAULT_AUTO_FIELD`` is not set.
- Miscellaneous documentation fixes.
- Miscellaneous internal improvements.
v2.2 (2019-12-03)
^^^^^^^^^^^^^^^^^
- **BACKWARD INCOMPATIBLE** Drop support for Python 3.4.
- **BACKWARD INCOMPATIBLE** Drop support for Django < 1.11.
- Add support for Django 3.0.
- Add support for Python 3.8.
- Add support for PyPy 3.
- Replace ``django.utils.six`` with ``six`` to support Django >= 3.
- Start using tox-travis and setuptools-scm for simplified test harness
and release management.
v2.1 (2018-08-16)
^^^^^^^^^^^^^^^^^
- **BACKWARD INCOMPATIBLE** Drop support of Python 3.3.
- **BACKWARD INCOMPATIBLE** Drop support of Django 1.9.
- Add support for Django 2.1.
- Add ``PositiveIntegerValue`` configuration value.
- Fix ``bool(BooleanValue)`` to behave as one would expect (e.g.
``bool(BooleanValue(False))`` returns ``False``).
- Miscellaneous documentation improvements and bug fixes.
v2.0 (2016-07-29)
^^^^^^^^^^^^^^^^^
- **BACKWARD INCOMPATIBLE** Drop support of Python 2.6 and 3.2
- **BACKWARD INCOMPATIBLE** Drop support of Django < 1.8
- **BACKWARD INCOMPATIBLE** Moved sphinx callable has been moved from
``configurations`` to ``configurations.sphinx``.
- **BACKWARD INCOMPATIBLE** Removed the previously deprecated
``configurations.Settings`` class in favor of the
``configurations.Configuration`` added in 0.4. This removal was planned for
the 1.0 release and is now finally enacted.
- Add multiprocessing support for sphinx integration
- Fix a RemovedInDjango19Warning warning
v1.0 (2016-01-04)
^^^^^^^^^^^^^^^^^
- Project has moved to `Jazzband <https://jazzband.co/>`_. See guidelines for
contributing.
- Support for Django 1.8 and above.
- Allow ``Value`` classes to be used outside of ``Configuration`` classes. (#62)
- Fixed "Value with ValidationMixin will raise ValueError if no default assigned". (#69)
- Fixed wrong behaviour when assigning BooleanValue. (#83)
- Add ability to programmatically call Django commands from configurations using
``call_command``.
- Added SingleNestedTupleValue and SingleNestedListValue classes. (#85)
- Several other miscellaneous bugfixes.
v0.8 (2014-01-16)
^^^^^^^^^^^^^^^^^
- Added ``SearchURLValue`` to configure Haystack ``HAYSTACK_CONNECTIONS``
settings.
v0.7 (2013-11-26)
^^^^^^^^^^^^^^^^^
- Removed the broken stdout wrapper that displayed the currently enabled
configuration when using the runserver management command. Added a logging
based solution instead.
- Fixed default value of ``CacheURLValue`` class that was shadowed by an
unneeded name parameter. Thanks to Stefan Wehrmeyer.
- Fixed command line options checking in the importer to happen before the
validation. Thanks to Stefan Wehrmeyer.
- Added Tox test configuration.
- Fixed an erroneous use of ``PathValue`` in the 1.6.x project template.
v0.6 (2013-09-19)
^^^^^^^^^^^^^^^^^
- Added a IPython extension to support IPython notebooks correctly. See
the :doc:`cookbook` for more information.
v0.5.1 (2013-09-12)
^^^^^^^^^^^^^^^^^^^
- Prevented accidentally parsing the command line options to look for the
``--configuration`` option outside of Django's management commands.
This should fix a problem with gunicorn's own ``--config`` option.
Thanks to Brian Rosner for the report.
v0.5 (2013-09-09)
^^^^^^^^^^^^^^^^^
- Switched from raising Django's ``ImproperlyConfigured`` exception on errors
to standard ``ValueError`` to prevent hiding those errors when Django
specially handles the first.
- Switched away from d2to1 as a way to define package metadata since distutils2
is dead.
- Extended ``Value`` class documentation and fixed other issues.
- Moved tests out of the ``configurations`` package for easier maintenance.
v0.4 (2013-09-03)
^^^^^^^^^^^^^^^^^
- Added ``Value`` classes and subclasses for easier handling of settings values,
including populating them from environment variables.
- Renamed ``configurations.Settings`` class to ``configurations.Configuration``
to better describe what the class is all about. The old class still exists
and is marked as pending deprecation. It'll be removed in version 1.0.
- Added a ``setup`` method to handle the new ``Value`` classes and allow an
in-between modification of the configuration values.
- Added Django project templates for 1.5.x and 1.6.x.
- Reorganized and extended documentation.
v0.3.2 (2014-01-16)
^^^^^^^^^^^^^^^^^^^
- Fixed an installation issue.
v0.3.1 (2013-09-20)
^^^^^^^^^^^^^^^^^^^
- Backported a fix from master that makes 0.3.x compatible with newer
versions of six.
v0.3 (2013-05-15)
^^^^^^^^^^^^^^^^^
- Added ``pristinemethod`` decorator to be able to have callables as settings.
- Added ``pre_setup`` and ``post_setup`` method hooks to be able to run code
before or after the settings loading is finished.
- Minor docs and tests cleanup.
v0.2.1 (2013-04-11)
^^^^^^^^^^^^^^^^^^^
- Fixed a regression in parsing the new ``-C``/``--configuration`` management
command option.
- Minor fix in showing the configuration in the ``runserver`` management
command output.
v0.2 (2013-03-27)
^^^^^^^^^^^^^^^^^
- **backward incompatible change** Dropped support for Python 2.5! Please use
the 0.1 version if you really want.
- Added Python>3.2 and Django 1.5 support!
- Catch error when getting or evaluating callable setting class attributes.
- Simplified and extended tests.
- Added optional ``-C``/``--configuration`` management command option similar
to Django's ``--settings`` option
- Fixed the runserver message about which setting is used to
show the correct class.
- Stopped hiding AttributeErrors happening during initialization
of settings classes.
- Added FastCGI helper.
- Minor documentation fixes
v0.1 (2012-07-21)
^^^^^^^^^^^^^^^^^
- Initial public release

View file

@ -1,44 +1,294 @@
import configurations
# -*- coding: utf-8 -*-
#
# django-configurations documentation build configuration file, created by
# sphinx-quickstart on Sat Jul 21 15:03:23 2012.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# -- Project information -----------------------------------------------------
project = 'django-configurations'
copyright = '2012-2023, Jannis Leidel and other contributors'
author = 'Jannis Leidel and other contributors'
import sys
import os
release = configurations.__version__
version = ".".join(release.split(".")[:2])
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ---------------------------------------------------
add_function_parentheses = False
add_module_names = False
# -- General configuration -----------------------------------------------------
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
]
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# 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']
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'sphinx': ('https://www.sphinx-doc.org/en/master', None),
'django': ('https://docs.djangoproject.com/en/dev',
'https://docs.djangoproject.com/en/dev/_objects/'),
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# -- Options for HTML output -------------------------------------------------
html_theme = 'furo'
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'django-configurations'
copyright = u'2012, Jannis Leidel'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
try:
from configurations import __version__
# The short X.Y version.
version = '.'.join(__version__.split('.')[:2])
# The full version, including alpha/beta/rc tags.
release = __version__
except ImportError:
version = release = 'dev'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-configurationsdoc'
# -- Options for Epub output ---------------------------------------------------
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
# (source start file, target name, title, author, documentclass)
('index', 'django-configurations.tex',
'django-configurations Documentation', author, 'manual'),
('index', 'django-configurations.tex', u'django-configurations Documentation',
u'Jannis Leidel', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'django-configurations', u'django-configurations Documentation',
[u'Jannis Leidel'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'django-configurations', u'django-configurations Documentation',
u'Jannis Leidel', 'django-configurations', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# -- Options for Epub output ---------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = u'django-configurations'
epub_author = u'Jannis Leidel'
epub_publisher = u'Jannis Leidel'
epub_copyright = u'2012, Jannis Leidel'
# The language of the text. It defaults to the language option
# or en if the language is not set.
#epub_language = ''
# The scheme of the identifier. Typical schemes are ISBN or URL.
#epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#epub_identifier = ''
# A unique identification for the text.
#epub_uid = ''
# A tuple containing the cover image and cover page html template filenames.
#epub_cover = ()
# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_pre_files = []
# HTML files shat should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
#epub_post_files = []
# A list of files that should not be packed into the epub file.
#epub_exclude_files = []
# The depth of the table of contents in toc.ncx.
#epub_tocdepth = 3
# Allow duplicate toc entries.
#epub_tocdup = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}

View file

@ -1,350 +0,0 @@
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'])
Read .env file
--------------
Configurations can read values for environment variables out of an ``.env``
file, and push them into the application's process environment. Simply set
the ``DOTENV`` setting to the appropriate file name:
.. code-block:: python
# mysite/settings.py
import os.path
from configurations import Configuration, values
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
class Dev(Configuration):
DOTENV = os.path.join(BASE_DIR, '.env')
SECRET_KEY = values.SecretValue()
API_KEY1 = values.Value()
API_KEY2 = values.Value()
API_KEY3 = values.Value('91011')
A ``.env`` file is a ``.ini``-style file. It must contain a list of
``KEY=value`` pairs, just like Shell environment variables:
.. code-block:: ini
# .env
DJANGO_DEBUG=False
DJANGO_SECRET_KEY=1q2w3e4r5t6z7u8i9o0(%&)$§!pqaycz
API_KEY1=1234
API_KEY2=5678
Envdir
------
envdir_ is an effective way to set a large number of environment variables
at once during startup of a command. This is great in combination with
django-configuration's :class:`~configurations.values.Value` subclasses
when enabling their ability to check environment variables for override
values.
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:
.. code-block:: console
$ tree --noreport mysite_env/
mysite_env/
├── DJANGO_SETTINGS_MODULE
├── DJANGO_DEBUG
├── DJANGO_DATABASE_URL
├── DJANGO_CACHE_URL
└── PYTHONSTARTUP
$ cat mysite_env/DJANGO_CACHE_URL
redis://user@host:port/1
Then, to enable the ``mysite_env`` environment variables, simply use the
``envdir`` command line tool as a prefix for your program, e.g.:
.. code-block:: console
$ envdir mysite_env python manage.py runserver
See envdir_ documentation for more information, e.g. using envdir_ from
Python instead of from the command line.
.. _envdir: https://pypi.python.org/pypi/envdir
Sentry (dynamic setup calls)
----------------------------
For all tools that require an initialization call you should use
:ref:`Setup methods<setup-methods>` (unless you want them activated
for all environments).
Intuitively you might want to add the required setup call like any
other setting:
.. code-block:: python
class Prod(Base):
# ...
sentry_sdk.init("your dsn", integrations=[DjangoIntegration()])
But this will activate, in this case, Sentry even when you're running a
Dev configuration. What you should do instead, is put that code in the
``post_setup`` function. That way Sentry will only ever run when Prod
is the selected configuration:
.. code-block:: python
class Prod(Base):
# ...
@classmethod
def post_setup(cls):
"""Sentry initialization"""
super(Prod, cls).post_setup()
sentry_sdk.init(
dsn=os.environ.get("your dsn"), integrations=[DjangoIntegration()]
)
.. _project-templates:
Project templates
-----------------
You can use a special Django project template that is a copy of the one
included in Django 1.5.x and 1.6.x. The following examples assumes you're
using pip_ to install packages.
Django 1.8.x
^^^^^^^^^^^^
First install Django 1.8.x and django-configurations:
.. code-block:: console
$ python -m pip install -r https://raw.github.com/jazzband/django-configurations/templates/1.8.x/requirements.txt
Or Django 1.8:
.. code-block:: console
$ python -m django startproject mysite -v2 --template https://github.com/jazzband/django-configurations/archive/templates/1.8.x.zip
Now you have a default Django 1.8.x project in the ``mysite``
directory that uses django-configurations.
See the repository of the template for more information:
https://github.com/jazzband/django-configurations/tree/templates/1.8.x
.. _pip: http://pip-installer.org/
Celery
------
< 3.1
^^^^^
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:
.. code-block:: python
import configurations
configurations.setup()
That has the same effect as using the ``manage.py``, ``wsgi.py`` or ``asgi.py`` utilities.
This will also call ``django.setup()``.
>= 3.1
^^^^^^
In Celery 3.1 and later the integration between Django and Celery has been
simplified to use the standard Celery Python API. Django projects using Celery
are now advised to add a ``celery.py`` file that instantiates an explicit
``Celery`` client app.
Here's how to integrate django-configurations following the `example from
Celery's documentation`_:
.. code-block:: python
:emphasize-lines: 9, 11-12
from __future__ import absolute_import
import os
from celery import Celery
from django.conf import settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'MySiteConfiguration')
import configurations
configurations.setup()
app = Celery('mysite')
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
@app.task(bind=True)
def debug_task(self):
print('Request: {0!r}'.format(self.request))
.. _`example from Celery's documentation`: http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html
iPython notebooks
-----------------
.. versionadded:: 0.6
To use django-configurations with IPython_'s great notebooks, you have to
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:
.. code-block:: console
$ ipython profile create
Then let IPython show you where the configuration file ``ipython_config.py``
was created:
.. 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:
.. 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.
.. _IPython: http://ipython.org/
.. _`manage your IPython profile`: http://ipython.org/ipython-doc/dev/config/overview.html#configuration-file-location
.. _shell_plus: https://django-extensions.readthedocs.io/en/latest/shell_plus.html
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:
.. code-block:: python
#!/usr/bin/env python
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'MySiteConfiguration')
from configurations.fastcgi import runfastcgi
runfastcgi(method='threaded', daemonize='true')
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
------
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.sphinx',
]
# ...
.. versionchanged:: 2.0
Please note that the sphinx callable has been moved from ``configurations`` to
``configurations.sphinx``.
Channels
--------
If you want to deploy a project that uses the Django channels with
`Daphne <http://github.com/django/daphne/>`_ as the
`interface server <http://channels.readthedocs.io/en/latest/deploying.html#run-interface-servers>`_
you have to use a asgi.py script similar to the following:
.. code-block:: python
import os
from configurations import importer
from channels.asgi import get_channel_layer
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings")
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
importer.install()
channel_layer = get_channel_layer()
That will properly load your django-configurations powered settings.

View file

@ -1,12 +1,5 @@
.. include:: ../README.rst
Project templates
^^^^^^^^^^^^^^^^^
Don't miss the Django :ref:`project templates pre-configured with
django-configurations<project-templates>` to simplify getting started
with new Django projects.
Wait, what?
-----------
@ -27,7 +20,7 @@ use of the ``from foo import *`` anti-pattern.
Okay, how does it work?
-----------------------
Any subclass of the ``configurations.Configuration`` class will automatically
Any subclass of the ``configurations.Settings`` class will automatically
use the values of its class and instance attributes (including properties
and methods) to set module level variables of the same module -- that's
how Django will interface to the django-configurations based settings during
@ -60,16 +53,83 @@ behind the scenes.
.. _`PEP 302`: http://www.python.org/dev/peps/pep-0302/
Further documentation
---------------------
Usage patterns
--------------
.. toctree::
:maxdepth: 3
There are various configuration patterns that can be implemented with
django-configurations. The most common pattern is to have a base class
and various subclasses based on the enviroment they are supposed to be
used in, e.g. in production, staging and development.
patterns
values
cookbook
changes
Server specific settings
^^^^^^^^^^^^^^^^^^^^^^^^
For example, imagine you have a base setting class in your **settings.py**
file::
from configurations import Settings
class Base(Settings):
TIME_ZONE = 'Europe/Berlin'
class Dev(Base):
DEBUG = True
TEMPLATE_DEBUG = DEBUG
class Prod(Base):
TIME_ZONE = 'America/New_York'
You can now set the ``DJANGO_CONFIGURATION`` environment variable to one
of the class names you've defined, e.g. on your production server it
should be ``Prod``. In bash that would be::
export DJANGO_SETTINGS_MODULE=mysite.settings
export DJANGO_CONFIGURATION=Prod
python manage.py runserver
Alternatively you can use the ``--configuration`` option when using Django
management commands along the lines of Django's default ``--settings``
command line option, e.g.::
python manage.py runserver --settings=mysite.settings --configuration=Prod
Global settings defaults
^^^^^^^^^^^^^^^^^^^^^^^^
Every ``configurations.Settings`` subclass will automatically contain
Django's global settings as class attributes, so you can refer to them when
setting other values, e.g.::
from configurations import Settings
class Base(Settings):
TEMPLATE_CONTEXT_PROCESSORS = \
Settings.TEMPLATE_CONTEXT_PROCESSORS + (
'django.core.context_processors.request',
)
@property
def LANGUAGES(self):
return Settings.LANGUAGES + (('tlh', 'Klingon'),)
Mixins
^^^^^^
You might want to apply some configuration values for each and every
project you're working on without having to repeat yourself. Just define
a few mixin you re-use multiple times::
class FullPageCaching(object):
USE_ETAGS = True
Then import that mixin class in your site settings module and use it with
a Settings class::
from configurations import Settings
class AcmeProd(Settings, FullPageCaching):
DEBUG = False
# ...
Alternatives
------------
@ -86,13 +146,54 @@ Many thanks to those project that have previously solved these problems:
.. _Pinax: http://pinaxproject.com
.. _`django-classbasedsettings`: https://github.com/matthewwithanm/django-classbasedsettings
Cookbook
--------
Celery
^^^^^^
Given Celery's way to load Django settings in worker processes you should
probably just add the following to the **begin** of your settings module::
from configurations import importer
importer.install()
That has the same effect as using the ``manage.py`` or ``wsgi.py`` utilities
mentioned above.
FastCGI
^^^^^^^
In case you use FastCGI for deploying Django (you really shouldn't) and aren't
allowed to us 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::
#!/usr/bin/env python
import os
import sys
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'MySiteSettings')
from configurations.fastcgi import runfastcgi
runfastcgi(method='threaded', daemonize='true')
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/
Bugs and feature requests
-------------------------
As always your mileage may vary, so please don't hesitate to send feature
requests and bug reports:
As always you mileage may vary, so please don't hesitate to send in feature
requests and bug reports at the usual place:
- https://github.com/jazzband/django-configurations/issues
https://github.com/jezdez/django-configurations/issues
Thanks!
Thanks!
.. include:: ../CHANGES.rst

190
docs/make.bat Normal file
View file

@ -0,0 +1,190 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-configurations.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-configurations.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

View file

@ -1,219 +0,0 @@
Usage patterns
==============
There are various configuration patterns that can be implemented with
django-configurations. The most common pattern is to have a base class
and various subclasses based on the environment they are supposed to be
used in, e.g. in production, staging and development.
Server specific settings
------------------------
For example, imagine you have a base setting class in your **settings.py**
file:
.. code-block:: python
from configurations import Configuration
class Base(Configuration):
TIME_ZONE = 'Europe/Berlin'
class Dev(Base):
DEBUG = True
class Prod(Base):
TIME_ZONE = 'America/New_York'
You can now set the ``DJANGO_CONFIGURATION`` environment variable to
one of the class names you've defined, e.g. on your production server
it should be ``Prod``. In Bash that would be:
.. code-block:: console
$ export DJANGO_SETTINGS_MODULE=mysite.settings
$ export DJANGO_CONFIGURATION=Prod
$ python -m manage runserver
Alternatively you can use the ``--configuration`` option when using Django
management commands along the lines of Django's default ``--settings``
command line option, e.g.
.. code-block:: console
$ python -m manage runserver --settings=mysite.settings --configuration=Prod
Property settings
-----------------
Use a ``property`` to allow for computed settings. This pattern can
also be used to postpone / lazy evaluate a value. E.g., useful when
nesting a Value in a dictionary and a string is required:
.. code-block:: python
class Prod(Configuration):
SOME_VALUE = values.Value(None, environ_prefix=None)
@property
def SOME_CONFIG(self):
return {
'some_key': self.SOME_VALUE,
}
Global settings defaults
------------------------
Every ``configurations.Configuration`` subclass will automatically
contain Django's global settings as class attributes, so you can refer
to them when setting other values, e.g.
.. code-block:: python
from configurations import Configuration
class Prod(Configuration):
TEMPLATE_CONTEXT_PROCESSORS = Configuration.TEMPLATE_CONTEXT_PROCESSORS + (
'django.core.context_processors.request',
)
@property
def LANGUAGES(self):
return list(Configuration.LANGUAGES) + [('tlh', 'Klingon')]
Configuration mixins
--------------------
You might want to apply some configuration values for each and every
project you're working on without having to repeat yourself. Just define
a few mixin you re-use multiple times:
.. code-block:: python
class FullPageCaching:
USE_ETAGS = True
Then import that mixin class in your site settings module and use it with
a ``Configuration`` class:
.. code-block:: python
from configurations import Configuration
class Prod(FullPageCaching, Configuration):
DEBUG = False
# ...
Pristine methods
----------------
.. versionadded:: 0.3
In case one of your settings itself need to be a callable, you need to
tell that django-configurations by using the ``pristinemethod``
decorator, e.g.
.. code-block:: python
from configurations import Configuration, pristinemethod
class Prod(Configuration):
@pristinemethod
def ACCESS_FUNCTION(user):
return user.is_staff
Lambdas work, too:
.. code-block:: python
from configurations import Configuration, pristinemethod
class Prod(Configuration):
ACCESS_FUNCTION = pristinemethod(lambda user: user.is_staff)
.. _setup-methods:
Setup methods
-------------
.. versionadded:: 0.3
If there is something required to be set up before, during or after the
settings loading happens, please override the ``pre_setup``, ``setup`` or
``post_setup`` class methods like so (don't forget to apply the Python
``@classmethod`` decorator):
.. code-block:: python
import logging
from configurations import Configuration
class Prod(Configuration):
# ...
@classmethod
def pre_setup(cls):
super(Prod, cls).pre_setup()
if something.completely.different():
cls.DEBUG = True
@classmethod
def setup(cls):
super(Prod, cls).setup()
logging.info('production settings loaded: %s', cls)
@classmethod
def post_setup(cls):
super(Prod, cls).post_setup()
logging.debug("done setting up! \o/")
As you can see above the ``pre_setup`` method can also be used to
programmatically change a class attribute of the settings class and it
will be taken into account when doing the rest of the settings setup.
Of course that won't work for ``post_setup`` since that's when the
settings setup is already done.
In fact you can easily do something unrelated to settings, like
connecting to a database:
.. code-block:: python
from configurations import Configuration
class Prod(Configuration):
# ...
@classmethod
def post_setup(cls):
import mango
mango.connect('enterprise')
.. warning::
You could do the same by overriding the ``__init__`` method of your
settings class but this may cause hard to debug errors because
at the time the ``__init__`` method is called (during Django
startup) the Django setting system isn't fully loaded yet.
So anything you do in ``__init__`` that may require
``django.conf.settings`` or Django models there is a good chance it
won't work. Use the ``post_setup`` method for that instead.
.. versionchanged:: 0.4
A new ``setup`` method was added to be able to handle the new
:class:`~configurations.values.Value` classes and allow an
in-between modification of the configuration values.
Standalone scripts
------------------
If you want to run scripts outside of your project you need to add
these lines on top of your file:
.. code-block:: python
import configurations
configurations.setup()

View file

@ -1,3 +0,0 @@
Sphinx>4
furo
docutils

View file

@ -1,648 +0,0 @@
Values
======
.. module:: configurations.values
:synopsis: Optional value classes for high-level validation and behavior.
.. versionadded:: 0.4
django-configurations allows you to optionally reduce the amount of validation
and setup code in your **settings.py** by using ``Value`` classes. They have
the ability to handle values from the process environment of your software
(:data:`os.environ`) and work well in projects that follow the
`Twelve-Factor methodology`_.
.. note::
These classes are required to be used as attributes of ``Configuration``
classes. See the :doc:`main documentation<index>` for more information.
Overview
--------
Here is an example (from a **settings.py** file with a ``Configuration``
subclass):
.. code-block:: python
:emphasize-lines: 4
from configurations import Configuration, values
class Dev(Configuration):
DEBUG = values.BooleanValue(True)
As you can see all you have to do is to wrap your settings value in a call
to one of the included values classes. When Django's process starts up
it will automatically make sure the passed-in value validates correctly --
in the above case checks if the value is really a boolean.
You can safely use other :class:`~Value` instances as the default setting
value:
.. code-block:: python
:emphasize-lines: 5
from configurations import Configuration, values
class Dev(Configuration):
DEBUG = values.BooleanValue(True)
DEBUG_PROPAGATE_EXCEPTIONS = values.BooleanValue(DEBUG)
See the list of :ref:`built-in value classes<built-ins>` for more information.
Environment variables
---------------------
To separate the site configuration from your application code you should use
environment variables for configuration. Unfortunately environment variables
are string based so they are not easily mapped to the Python based settings
system Django uses.
Luckily django-configurations' :class:`~Value` subclasses have the ability
to handle environment variables for the common use cases.
Default behavior
^^^^^^^^^^^^^^^^
For example, imagine you want to override the ``ROOT_URLCONF`` setting on your
staging server to be able to debug a problem with your in-development code.
You're using a web server that passes the environment variables from
the shell it was started from into your Django WSGI process.
Use the boolean ``environ`` option of the :class:`~Value` class (``True`` by
default) to tell django-configurations to look for an environment variable with
the same name as the specific :class:`~Value` variable, only uppercased and
prefixed with ``DJANGO_``. E.g.:
.. code-block:: python
:emphasize-lines: 5
from configurations import Configuration, values
class Stage(Configuration):
# ..
ROOT_URLCONF = values.Value('mysite.urls')
django-configurations will try to read the ``DJANGO_ROOT_URLCONF`` environment
variable when deciding which value the ``ROOT_URLCONF`` setting should have.
When you run the web server simply specify that environment variable
(e.g. in your init script):
.. code-block:: console
$ DJANGO_ROOT_URLCONF=mysite.debugging_urls gunicorn mysite.wsgi:application
If the environment variable can't be found it'll use the default
``'mysite.urls'``.
Disabling environment variables
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To disable environment variables, specify the ``environ`` parameter of the
:class:`~Value` class. For example this would disable it for the ``TIME_ZONE``
setting value::
from configurations import Configuration, values
class Dev(Configuration):
TIME_ZONE = values.Value('UTC', environ=False)
Custom environment variable names
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To support legacy systems, integrate with other parts of your software stack or
simply better match your taste in naming public configuration variables,
django-configurations allows you to use the ``environ_name`` parameter of the
:class:`~Value` class to change the base name of the environment variable it
looks for. For example this would enforce the name ``DJANGO_MYSITE_TZ``
instead of using the name of the :class:`~Value` instance.::
from configurations import Configuration, values
class Dev(Configuration):
TIME_ZONE = values.Value('UTC', environ_name='MYSITE_TZ')
Allow final value to be used outside the configuration context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You may use the ``environ_name`` parameter to allow a :class:`~Value` to be
directly converted to its final value for use outside of the configuration
context:
.. code-block:: pycon
>>> type(values.Value([]))
<class 'configurations.values.Value'>
>>> type(values.Value([], environ_name="FOOBAR"))
<class 'list'>
This can also be achieved when using ``environ=False`` and providing a
default value.
Custom environment variable prefixes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In case you want to change the default environment variable name prefix
of ``DJANGO`` to something to your likening, use the ``environ_prefix``
parameter of the :class:`~Value` instance. Here it'll look for the
``MYSITE_TIME_ZONE`` environment variable (instead of ``DJANGO_TIME_ZONE``)::
from configurations import Configuration, values
class Dev(Configuration):
TIME_ZONE = values.Value('UTC', environ_prefix='MYSITE')
The ``environ_prefix`` parameter can also be ``None`` to completely disable
the prefix.
``Value`` class
---------------
.. class:: Value(default, [environ=True, environ_name=None, environ_prefix='DJANGO', environ_required=False])
The ``Value`` class takes one required and several optional parameters.
:param default: the default value of the setting
:param environ: toggle for environment use
:param environ_name: capitalized name of environment variable to look for
:param environ_prefix: capitalized prefix to use when looking for environment variable
:param environ_required: whether or not the value is required to be set as an environment variable
:type environ: bool
:type environ_name: str or None
:type environ_prefix: str
:type environ_required: bool
The ``default`` parameter is effectively the value the setting has
right now in your ``settings.py``.
.. method:: setup(name)
:param name: the name of the setting
:return: setting value
The ``setup`` method is called during startup of the Django process and
implements the ability to check the environment variable. Its purpose is
to return a value django-configurations is supposed to use when loading
the settings. It'll be passed one parameter, the name of the
:class:`~Value` instance as defined in the ``settings.py``. This is used
for building the name of the environment variable.
.. method:: to_python(value)
:param value: the value of the setting as found in the process
environment (:data:`os.environ`)
:return: validated and "ready" setting value if found in process
environment
The ``to_python`` method is used when the ``environ`` parameter of the
:class:`~Value` class is set to ``True`` (the default) and an
environment variable with the appropriate name was found.
It will be used to handle the string based environment variables and
returns the "ready" value of the setting.
Some :class:`~Value` subclasses also use it during initialization when the
default value has a string-like format like an environment variable which
needs to be converted into a Python data type.
.. _built-ins:
Built-ins
---------
Type values
^^^^^^^^^^^
.. class:: BooleanValue
A :class:`~Value` subclass that checks and returns boolean values. Possible
values for environment variables are:
- ``True`` values: ``'yes'``, ``'y'``, ``'true'``, ``'1'``
- ``False`` values: ``'no'``, ``'n'``, ``'false'``, ``'0'``,
``''`` (empty string)
::
DEBUG = values.BooleanValue(True)
.. class:: IntegerValue
A :class:`~Value` subclass that handles integer values.
::
MYSITE_CACHE_TIMEOUT = values.IntegerValue(3600)
.. class:: PositiveIntegerValue
A :class:`~Value` subclass that handles positive integer values.
.. versionadded:: 2.1
::
MYSITE_WORKER_POOL = values.PositiveIntegerValue(8)
.. class:: FloatValue
A :class:`~Value` subclass that handles float values.
::
MYSITE_TAX_RATE = values.FloatValue(11.9)
.. class:: DecimalValue
A :class:`~Value` subclass that handles Decimal values.
::
MYSITE_CONVERSION_RATE = values.DecimalValue(decimal.Decimal('4.56214'))
.. class:: SequenceValue
Common base class for sequence values.
.. class:: ListValue(default, [separator=',', converter=None])
A :class:`~SequenceValue` subclass that handles list values.
:param separator: the separator to split environment variables with
:param converter: the optional converter callable to apply for each list
item
Simple example::
ALLOWED_HOSTS = ListValue(['mysite.com', 'mysite.biz'])
Use a custom converter to check for the given variables::
def check_monty_python(person):
if not is_completely_different(person):
error = '{0} is not a Monty Python member'.format(person)
raise ValueError(error)
return person
MONTY_PYTHONS = ListValue(['John Cleese', 'Eric Idle'],
converter=check_monty_python)
You can override this list with an environment variable like this:
.. code-block:: console
$ DJANGO_MONTY_PYTHONS="Terry Jones,Graham Chapman" gunicorn mysite.wsgi:application
Use a custom separator::
EMERGENCY_EMAILS = ListValue(['admin@mysite.net'], separator=';')
And override it:
.. code-block:: console
$ DJANGO_EMERGENCY_EMAILS="admin@mysite.net;manager@mysite.org;support@mysite.com" gunicorn mysite.wsgi:application
.. class:: TupleValue
A :class:`~SequenceValue` subclass that handles tuple values.
:param separator: the separator to split environment variables with
:param converter: the optional converter callable to apply for each tuple
item
See the :class:`~ListValue` examples above.
.. class:: SingleNestedSequenceValue
Common base class for nested sequence values.
.. class:: SingleNestedTupleValue(default, [seq_separator=';', separator=',', converter=None])
A :class:`~SingleNestedSequenceValue` subclass that handles single nested tuple values,
e.g. ``((a, b), (c, d))``.
:param seq_separator: the separator to split each tuple with
:param separator: the separator to split the inner tuple contents with
:param converter: the optional converter callable to apply for each inner
tuple item
Useful for ADMINS, MANAGERS, and the like. For example::
ADMINS = SingleNestedTupleValue((
('John', 'jcleese@site.com'),
('Eric', 'eidle@site.com'),
))
Override using environment variables like this::
DJANGO_ADMINS=Terry,tjones@site.com;Graham,gchapman@site.com
.. class:: SingleNestedListValue(default, [seq_separator=';', separator=',', converter=None])
A :class:`~SingleNestedSequenceValue` subclass that handles single nested list values,
e.g. ``[[a, b], [c, d]]``.
:param seq_separator: the separator to split each list with
:param separator: the separator to split the inner list contents with
:param converter: the optional converter callable to apply for each inner
list item
See the :class:`~SingleNestedTupleValue` examples above.
.. class:: SetValue
A :class:`~Value` subclass that handles set values.
:param separator: the separator to split environment variables with
:param converter: the optional converter callable to apply for each set
item
See the :class:`~ListValue` examples above.
.. class:: DictValue
A :class:`~Value` subclass that handles dicts.
::
DEPARTMENTS = values.DictValue({
'it': ['Mike', 'Joe'],
})
Override using environment variables like this::
DJANGO_DEPARTMENTS={'it':['Mike','Joe'],'hr':['Emma','Olivia']}
Validator values
^^^^^^^^^^^^^^^^
.. class:: EmailValue
A :class:`~Value` subclass that validates the value using the
:data:`django:django.core.validators.validate_email` validator.
::
SUPPORT_EMAIL = values.EmailValue('support@mysite.com')
.. class:: URLValue
A :class:`~Value` subclass that validates the value using the
:class:`django:django.core.validators.URLValidator` validator.
::
SUPPORT_URL = values.URLValue('https://support.mysite.com/')
.. class:: IPValue
A :class:`~Value` subclass that validates the value using the
:data:`django:django.core.validators.validate_ipv46_address` validator.
::
LOADBALANCER_IP = values.IPValue('127.0.0.1')
.. class:: RegexValue(default, regex, [environ=True, environ_name=None, environ_prefix='DJANGO'])
A :class:`~Value` subclass that validates according a regular expression
and uses the :class:`django:django.core.validators.RegexValidator`.
:param regex: the regular expression
::
DEFAULT_SKU = values.RegexValue('000-000-00', regex=r'\d{3}-\d{3}-\d{2}')
.. class:: PathValue(default, [check_exists=True, environ=True, environ_name=None, environ_prefix='DJANGO'])
A :class:`~Value` subclass that normalizes the given path using
:func:`os.path.expanduser` and checks if it exists on the file system.
Takes an optional ``check_exists`` parameter to disable the check with
:func:`os.path.exists`.
:param check_exists: toggle the file system check
::
BASE_DIR = values.PathValue('/opt/mysite/')
STATIC_ROOT = values.PathValue('/var/www/static', checks_exists=False)
URL-based values
^^^^^^^^^^^^^^^^
.. note::
The following URL-based :class:`~Value` subclasses are inspired by the
`Twelve-Factor methodology`_ and use environment variable names that are
already established by that methodology, e.g. ``'DATABASE_URL'``.
Each of these classes require external libraries to be installed, e.g. the
:class:`~DatabaseURLValue` class depends on the package ``dj-database-url``.
See the specific class documentation below for which package is needed.
.. class:: DatabaseURLValue(default, [alias='default', environ=True, environ_name='DATABASE_URL', environ_prefix=None])
A :class:`~Value` subclass that uses the `dj-database-url`_ app to
convert a database configuration value stored in the ``DATABASE_URL``
environment variable into an appropriate setting value. It's inspired by
the `Twelve-Factor methodology`_.
By default this :class:`~Value` subclass looks for the ``DATABASE_URL``
environment variable.
Takes an optional ``alias`` parameter to define which database alias to
use for the ``DATABASES`` setting.
:param alias: which database alias to use
The other parameters have the following default values:
:param environ: ``True``
:param environ_name: ``DATABASE_URL``
:param environ_prefix: ``None``
::
DATABASES = values.DatabaseURLValue('postgres://myuser@localhost/mydb')
.. _`dj-database-url`: https://pypi.python.org/pypi/dj-database-url/
.. class:: CacheURLValue(default, [alias='default', environ=True, environ_name='CACHE_URL', environ_prefix=None])
A :class:`~Value` subclass that uses the `django-cache-url`_ app to
convert a cache configuration value stored in the ``CACHE_URL``
environment variable into an appropriate setting value. It's inspired by
the `Twelve-Factor methodology`_.
By default this :class:`~Value` subclass looks for the ``CACHE_URL``
environment variable.
Takes an optional ``alias`` parameter to define which database alias to
use for the ``CACHES`` setting.
:param alias: which cache alias to use
The other parameters have the following default values:
:param environ: ``True``
:param environ_name: ``CACHE_URL``
:param environ_prefix: ``None``
::
CACHES = values.CacheURLValue('memcached://127.0.0.1:11211/')
.. _`django-cache-url`: https://pypi.python.org/pypi/django-cache-url/
.. class:: EmailURLValue(default, [environ=True, environ_name='EMAIL_URL', environ_prefix=None])
A :class:`~Value` subclass that uses the `dj-email-url`_ app to
convert an email configuration value stored in the ``EMAIL_URL``
environment variable into the appropriate settings. It's inspired by
the `Twelve-Factor methodology`_.
By default this :class:`~Value` subclass looks for the ``EMAIL_URL``
environment variable.
.. note::
This is a special value since email settings are divided into many
different settings variables. `dj-email-url`_ supports all options
though and simply returns a nested dictionary of settings instead of
just one setting.
The parameters have the following default values:
:param environ: ``True``
:param environ_name: ``EMAIL_URL``
:param environ_prefix: ``None``
::
EMAIL = values.EmailURLValue('console://')
.. _`dj-email-url`: https://pypi.python.org/pypi/dj-email-url/
.. class:: SearchURLValue(default, [environ=True, environ_name='SEARCH_URL', environ_prefix=None])
.. versionadded:: 0.8
A :class:`~Value` subclass that uses the `dj-search-url`_ app to
convert a search configuration value stored in the ``SEARCH_URL``
environment variable into the appropriate settings for use with Haystack_.
It's inspired by the `Twelve-Factor methodology`_.
By default this :class:`~Value` subclass looks for the ``SEARCH_URL``
environment variable.
Takes an optional ``alias`` parameter to define which search backend alias
to use for the ``HAYSTACK_CONNECTIONS`` setting.
:param alias: which cache alias to use
The other parameters have the following default values:
:param environ: ``True``
:param environ_name: ``SEARCH_URL``
:param environ_prefix: ``None``
::
HAYSTACK_CONNECTIONS = values.SearchURLValue('elasticsearch://127.0.0.1:9200/my-index')
.. _`dj-search-url`: https://pypi.python.org/pypi/dj-search-url/
.. _Haystack: http://haystacksearch.org/
Other values
^^^^^^^^^^^^
.. class:: BackendsValue
A :class:`~ListValue` subclass that validates the given list of dotted
import paths by trying to import them. In other words, this checks if
the backends exist.
::
MIDDLEWARE = values.BackendsValue([
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
])
.. class:: SecretValue
A :class:`~Value` subclass that doesn't allow setting a default value
during instantiation and force-enables the use of an environment variable
to reduce the risk of accidentally storing secret values in the settings
file. This usually resolves to ``DJANGO_SECRET_KEY`` unless you have
customized the environment variable names.
:raises: ``ValueError`` when given a default value
.. versionchanged:: 1.0
This value class has the ``environ_required`` parameter turned to
``True``.
::
SECRET_KEY = values.SecretValue()
Value mixins
^^^^^^^^^^^^
.. class:: CastingMixin
A mixin to be used with one of the :class:`~Value` subclasses that
requires a ``caster`` class attribute of one of the following types:
- dotted import path, e.g. ``'mysite.utils.custom_caster'``
- a callable, e.g. :class:`int`
Example::
class TemparatureValue(CastingMixin, Value):
caster = 'mysite.temperature.fahrenheit_to_celcius'
Optionally it can take a ``message`` class attribute as the error
message to be shown if the casting fails. Additionally an ``exception``
parameter can be set to a single or a tuple of exception classes that
are required to be handled during the casting.
.. class:: ValidationMixin
A mixin to be used with one of the :class:`~Value` subclasses that
requires a ``validator`` class attribute of one of the following types:
The validator should raise Django's
:exc:`~django.core.exceptions.ValidationError` to indicate a failed
validation attempt.
- dotted import path, e.g. ``'mysite.validators.custom_validator'``
- a callable, e.g. :class:`bool`
Example::
class TemparatureValue(ValidationMixin, Value):
validator = 'mysite.temperature.is_valid_temparature'
Optionally it can take a ``message`` class attribute as the error
message to be shown if the validation fails.
.. class:: MultipleMixin
A mixin to be used with one of the :class:`~Value` subclasses that
enables the return value of the :func:`~Value.to_python` to be
interpreted as a dictionary of settings values to be set at once,
instead of using the return value to just set one setting.
A good example for this mixin is the :class:`~EmailURLValue` value
which requires setting many ``EMAIL_*`` settings.
.. _`Twelve-Factor methodology`: http://www.12factor.net/

11
manage.py Executable file
View file

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

4
requirements/tests.txt Normal file
View file

@ -0,0 +1,4 @@
flake8
coverage
django-discover-runner
mock

View file

@ -1,10 +1,42 @@
[coverage:run]
source = .
branch = 1
parallel = 1
[coverage:report]
include = configurations/*,tests/*
[metadata]
name = django-configurations
version = 0.2
author = Jannis Leidel
author-email = jannis@leidel.info
summary = A helper for organizing Django settings.
description-file = README.rst
license = BSD
requires-dist = six
home-page = http://django-configurations.readthedocs.org/
project-url =
Github, https://github.com/jezdez/django-configurations/
classifier =
Development Status :: 4 - Beta
Environment :: Web Environment
Framework :: Django
Intended Audience :: Developers
License :: OSI Approved :: BSD License
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3.2
Programming Language :: Python :: 3.3
Topic :: Utilities
[flake8]
exclude = .tox,docs/*,.eggs
ignore = E501,E127,E128,E124,W503
[files]
packages =
configurations
configurations.tests
configurations.tests.settings
extra_files =
README.rst
CHANGES.rst
AUTHORS
.travis.yml
manage.py
Makefile
requirements/tests.txt
[backwards_compat]
zip_safe = False

View file

@ -1,71 +1,4 @@
import os
import codecs
#!/usr/bin/env python
from setuptools import setup
def read(*parts):
filename = os.path.join(os.path.dirname(__file__), *parts)
with codecs.open(filename, encoding='utf-8') as fp:
return fp.read()
setup(
name="django-configurations",
use_scm_version={"version_scheme": "post-release", "local_scheme": "dirty-tag"},
setup_requires=["setuptools_scm"],
url='https://django-configurations.readthedocs.io/',
project_urls={
'Source': 'https://github.com/jazzband/django-configurations',
},
license='BSD',
description="A helper for organizing Django settings.",
long_description=read('README.rst'),
long_description_content_type='text/x-rst',
author='Jannis Leidel',
author_email='jannis@leidel.info',
packages=['configurations'],
entry_points={
'console_scripts': [
'django-cadmin = configurations.management:execute_from_command_line',
],
},
install_requires=[
'django>=3.2',
],
python_requires='>=3.9, <4.0',
extras_require={
'cache': ['django-cache-url'],
'database': ['dj-database-url'],
'email': ['dj-email-url'],
'search': ['dj-search-url'],
'testing': [
'django-cache-url>=1.0.0',
'dj-database-url',
'dj-email-url',
'dj-search-url',
],
},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Framework :: Django',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Framework :: Django :: 5.1',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Utilities',
],
zip_safe=False,
)
setup(setup_requires=['d2to1'], d2to1=True)

View file

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

0
test_project/manage.py Executable file → Normal file
View file

View file

@ -1,17 +1,16 @@
from configurations import Configuration, values
from configurations import Settings
class Base(Configuration):
class Base(Settings):
# Django settings for test_project project.
DEBUG = values.BooleanValue(True, environ=True)
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@example.com'),
)
EMAIL_URL = values.EmailURLValue('console://', environ=True)
MANAGERS = ADMINS
DATABASES = {
@ -83,6 +82,7 @@ class Base(Configuration):
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
@ -94,7 +94,7 @@ class Base(Configuration):
'django.template.loaders.app_directories.Loader',
)
MIDDLEWARE = (
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@ -127,6 +127,7 @@ class Base(Configuration):
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'configurations',
'django_extensions',
)
# A sample logging configuration. The only tangible logging
@ -158,6 +159,21 @@ 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',
'django_extensions',
)
class Debug(Base):
YEAH = True

View file

@ -1,4 +1,4 @@
from django.conf.urls import patterns
from django.conf.urls import patterns, include, url
# Uncomment the next two lines to enable the admin:
# from django.contrib import admin

View file

@ -20,7 +20,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application # noqa
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# Apply WSGI middleware here.

View file

@ -1,9 +0,0 @@
from configurations import Configuration
def test_callback(request):
return {}
class Base(Configuration):
pass

View file

@ -1,11 +0,0 @@
from configurations import Configuration, values
class DotEnvConfiguration(Configuration):
DOTENV = 'test_project/.env'
DOTENV_VALUE = values.Value()
def DOTENV_VALUE_METHOD(self):
return values.Value(environ_name="DOTENV_VALUE")

View file

@ -1,8 +0,0 @@
from configurations import Configuration
class ErrorConfiguration(Configuration):
@classmethod
def pre_setup(cls):
raise ValueError("Error in pre_setup")

View file

@ -1,70 +0,0 @@
import os
import uuid
from configurations import Configuration, pristinemethod
class Test(Configuration):
BASE_DIR = os.path.abspath(
os.path.join(os.path.dirname(
os.path.abspath(__file__)), os.pardir))
DEBUG = True
SITE_ID = 1
SECRET_KEY = str(uuid.uuid4())
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(os.path.dirname(__file__), 'test.db'),
}
}
INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.auth',
'tests',
]
ROOT_URLCONF = 'tests.urls'
@property
def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:]
allowed_hosts.append('base')
return allowed_hosts
ATTRIBUTE_SETTING = True
_PRIVATE_SETTING = 'ryan'
@property
def PROPERTY_SETTING(self):
return 1
def METHOD_SETTING(self):
return 2
LAMBDA_SETTING = lambda self: 3 # noqa: E731
PRISTINE_LAMBDA_SETTING = pristinemethod(lambda: 4)
@pristinemethod
def PRISTINE_FUNCTION_SETTING():
return 5
@classmethod
def pre_setup(cls):
cls.PRE_SETUP_TEST_SETTING = 6
@classmethod
def post_setup(cls):
cls.POST_SETUP_TEST_SETTING = 7
class TestWithDefaultSetExplicitely(Test):
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View file

@ -1,26 +0,0 @@
from configurations import Configuration
class Mixin1:
@property
def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:]
allowed_hosts.append('test1')
return allowed_hosts
class Mixin2:
@property
def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:]
allowed_hosts.append('test2')
return allowed_hosts
class Inheritance(Mixin2, Mixin1, Configuration):
def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:]
allowed_hosts.append('test3')
return allowed_hosts

View file

@ -1,9 +0,0 @@
from .single_inheritance import Inheritance as BaseInheritance
class Inheritance(BaseInheritance):
def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:]
allowed_hosts.append('test-test')
return allowed_hosts

View file

@ -1,10 +0,0 @@
from .base import Base
class Inheritance(Base):
@property
def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:]
allowed_hosts.append('test')
return allowed_hosts

View file

@ -1,14 +0,0 @@
"""Used by tests to ensure logging is kept when calling setup() twice."""
from unittest import mock
import configurations
print('setup_1')
configurations.setup()
with mock.patch('django.setup', side_effect=Exception('setup called twice')):
print('setup_2')
configurations.setup()
print('setup_done')

View file

@ -1,15 +0,0 @@
import os
from django.test import TestCase
from unittest.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_VALUE_METHOD, 'is set')
self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV)

View file

@ -1,22 +0,0 @@
import os
from django.test import TestCase
from unittest.mock import patch
class ErrorTests(TestCase):
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='ErrorConfiguration',
DJANGO_SETTINGS_MODULE='tests.settings.error')
def test_env_loaded(self):
with self.assertRaises(ValueError) as cm:
from tests.settings import error # noqa: F401
self.assertIsInstance(cm.exception, ValueError)
self.assertEqual(
cm.exception.args,
(
"Couldn't setup configuration "
"'tests.settings.error.ErrorConfiguration': Error in pre_setup ",
)
)

View file

@ -1,38 +0,0 @@
import os
from django.test import TestCase
from unittest.mock import patch
class InheritanceTests(TestCase):
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='Inheritance',
DJANGO_SETTINGS_MODULE='tests.settings.single_inheritance')
def test_inherited(self):
from tests.settings import single_inheritance
self.assertEqual(
single_inheritance.ALLOWED_HOSTS,
['test']
)
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='Inheritance',
DJANGO_SETTINGS_MODULE='tests.settings.multiple_inheritance')
def test_inherited2(self):
from tests.settings import multiple_inheritance
self.assertEqual(
multiple_inheritance.ALLOWED_HOSTS,
['test', 'test-test']
)
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='Inheritance',
DJANGO_SETTINGS_MODULE='tests.settings.mixin_inheritance')
def test_inherited3(self):
from tests.settings import mixin_inheritance
self.assertEqual(
mixin_inheritance.ALLOWED_HOSTS,
['test1', 'test2', 'test3']
)

View file

@ -1,158 +0,0 @@
import os
import subprocess
import sys
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from unittest.mock import patch
from configurations.importer import ConfigurationFinder
ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
TEST_PROJECT_DIR = os.path.join(ROOT_DIR, 'test_project')
class MainTests(TestCase):
def test_simple(self):
from tests.settings import main
self.assertEqual(main.ATTRIBUTE_SETTING, True)
self.assertEqual(main.PROPERTY_SETTING, 1)
self.assertEqual(main.METHOD_SETTING, 2)
self.assertEqual(main.LAMBDA_SETTING, 3)
self.assertNotEqual(main.PRISTINE_LAMBDA_SETTING, 4)
self.assertTrue(lambda: callable(main.PRISTINE_LAMBDA_SETTING))
self.assertNotEqual(main.PRISTINE_FUNCTION_SETTING, 5)
self.assertTrue(lambda: callable(main.PRISTINE_FUNCTION_SETTING))
self.assertEqual(main.ALLOWED_HOSTS, ['base'])
self.assertEqual(main.PRE_SETUP_TEST_SETTING, 6)
self.assertRaises(AttributeError, lambda: main.POST_SETUP_TEST_SETTING)
self.assertEqual(main.Test.POST_SETUP_TEST_SETTING, 7)
def test_global_arrival(self):
from django.conf import settings
self.assertEqual(settings.PROPERTY_SETTING, 1)
self.assertRaises(AttributeError, lambda: settings._PRIVATE_SETTING)
self.assertNotEqual(settings.PRISTINE_LAMBDA_SETTING, 4)
self.assertTrue(lambda: callable(settings.PRISTINE_LAMBDA_SETTING))
self.assertNotEqual(settings.PRISTINE_FUNCTION_SETTING, 5)
self.assertTrue(lambda: callable(settings.PRISTINE_FUNCTION_SETTING))
self.assertEqual(settings.PRE_SETUP_TEST_SETTING, 6)
@patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test')
def test_empty_module_var(self):
with self.assertRaises(ImproperlyConfigured):
ConfigurationFinder()
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main')
def test_empty_class_var(self):
with self.assertRaises(ImproperlyConfigured):
ConfigurationFinder()
def test_global_settings(self):
from configurations.base import Configuration
self.assertIn('dictConfig', Configuration.LOGGING_CONFIG)
self.assertEqual(repr(Configuration),
"<Configuration 'configurations.base.Configuration'>")
def test_deprecated_settings_but_set_by_user(self):
from tests.settings.main import TestWithDefaultSetExplicitely
TestWithDefaultSetExplicitely.setup()
self.assertEqual(TestWithDefaultSetExplicitely.DEFAULT_AUTO_FIELD,
"django.db.models.BigAutoField")
def test_repr(self):
from tests.settings.main import Test
self.assertEqual(repr(Test),
"<Configuration 'tests.settings.main.Test'>")
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main',
DJANGO_CONFIGURATION='Test')
def test_initialization(self):
finder = ConfigurationFinder()
self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(finder.name, 'Test')
self.assertEqual(
repr(finder),
"<ConfigurationFinder for 'tests.settings.main.Test'>")
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.inheritance',
DJANGO_CONFIGURATION='Inheritance')
def test_initialization_inheritance(self):
finder = ConfigurationFinder()
self.assertEqual(finder.module,
'tests.settings.inheritance')
self.assertEqual(finder.name, 'Inheritance')
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main',
DJANGO_CONFIGURATION='NonExisting')
@patch.object(sys, 'argv', ['python', 'manage.py', 'test',
'--settings=tests.settings.main',
'--configuration=Test'])
def test_configuration_option(self):
finder = ConfigurationFinder(check_options=False)
self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(finder.name, 'NonExisting')
finder = ConfigurationFinder(check_options=True)
self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(finder.name, 'Test')
def test_configuration_argument_in_cli(self):
"""
Verify that's configuration option has been added to managements
commands
"""
proc = subprocess.Popen(['django-cadmin', 'test', '--help'],
stdout=subprocess.PIPE)
self.assertIn('--configuration', proc.communicate()[0].decode('utf-8'))
proc = subprocess.Popen(['django-cadmin', 'runserver', '--help'],
stdout=subprocess.PIPE)
self.assertIn('--configuration', proc.communicate()[0].decode('utf-8'))
def test_configuration_argument_in_runypy_cli(self):
"""
Verify that's configuration option has been added to managements
commands when using the -m entry point
"""
proc = subprocess.Popen(
[sys.executable, '-m', 'configurations', 'test', '--help'],
stdout=subprocess.PIPE,
)
self.assertIn('--configuration', proc.communicate()[0].decode('utf-8'))
proc = subprocess.Popen(
[sys.executable, '-m', 'configurations', 'runserver', '--help'],
stdout=subprocess.PIPE,
)
self.assertIn('--configuration', proc.communicate()[0].decode('utf-8'))
def test_django_setup_only_called_once(self):
proc = subprocess.Popen(
[sys.executable, os.path.join(os.path.dirname(__file__),
'setup_test.py')],
stdout=subprocess.PIPE)
res = proc.communicate()
stdout = res[0].decode('utf-8')
self.assertIn('setup_1', stdout)
self.assertIn('setup_2', stdout)
self.assertIn('setup_done', stdout)
self.assertEqual(proc.returncode, 0)
def test_utils_reraise(self):
from configurations.utils import reraise
class CustomException(Exception):
pass
with self.assertRaises(CustomException) as cm:
try:
raise CustomException
except Exception as exc:
reraise(exc, "Couldn't setup configuration", None)
self.assertEqual(cm.exception.args, ("Couldn't setup configuration: ",))

View file

@ -1,528 +0,0 @@
import decimal
import os
from contextlib import contextmanager
from django import VERSION as DJANGO_VERSION
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from unittest.mock import patch
from configurations.values import (Value, BooleanValue, IntegerValue,
FloatValue, DecimalValue, ListValue,
TupleValue, SingleNestedTupleValue,
SingleNestedListValue, SetValue,
DictValue, URLValue, EmailValue, IPValue,
RegexValue, PathValue, SecretValue,
DatabaseURLValue, EmailURLValue,
CacheURLValue, BackendsValue,
CastingMixin, SearchURLValue,
setup_value, PositiveIntegerValue)
@contextmanager
def env(**kwargs):
with patch.dict(os.environ, clear=True, **kwargs):
yield
class FailingCasterValue(CastingMixin, Value):
caster = 'non.existing.caster'
class ValueTests(TestCase):
def test_value_with_default(self):
value = Value('default', environ=False)
self.assertEqual(type(value), str)
self.assertEqual(value, 'default')
self.assertEqual(str(value), 'default')
def test_value_with_default_and_late_binding(self):
value = Value('default', environ=False, late_binding=True)
self.assertEqual(type(value), Value)
with env(DJANGO_TEST='override'):
self.assertEqual(value.setup('TEST'), 'default')
value = Value(environ_name='TEST')
self.assertEqual(type(value), str)
self.assertEqual(value, 'override')
self.assertEqual(str(value), 'override')
self.assertEqual(f'{value}', 'override')
self.assertEqual('%s' % value, 'override')
value = Value(environ_name='TEST', late_binding=True)
self.assertEqual(type(value), Value)
self.assertEqual(value.value, 'override')
self.assertEqual(str(value), 'override')
self.assertEqual(f'{value}', 'override')
self.assertEqual('%s' % value, 'override')
self.assertEqual(repr(value), repr('override'))
def test_value_truthy(self):
value = Value('default')
self.assertTrue(bool(value))
def test_value_falsey(self):
value = Value()
self.assertFalse(bool(value))
@patch.dict(os.environ, clear=True, DJANGO_TEST='override')
def test_env_var(self):
value = Value('default')
self.assertEqual(value.setup('TEST'), 'override')
self.assertEqual(str(value), 'override')
self.assertNotEqual(value.setup('TEST'), value.default)
self.assertEqual(value.to_python(os.environ['DJANGO_TEST']),
value.setup('TEST'))
def test_value_reuse(self):
value1 = Value('default')
value2 = Value(value1)
self.assertEqual(value1.setup('TEST1'), 'default')
self.assertEqual(value2.setup('TEST2'), 'default')
with env(DJANGO_TEST1='override1', DJANGO_TEST2='override2'):
self.assertEqual(value1.setup('TEST1'), 'override1')
self.assertEqual(value2.setup('TEST2'), 'override2')
def test_value_var_equal(self):
value1 = Value('default')
value2 = Value('default')
self.assertEqual(value1, value2)
self.assertTrue(value1 in ['default'])
def test_env_var_prefix(self):
with patch.dict(os.environ, clear=True, ACME_TEST='override'):
value = Value('default', environ_prefix='ACME')
self.assertEqual(value.setup('TEST'), 'override')
with patch.dict(os.environ, clear=True, TEST='override'):
value = Value('default', environ_prefix='')
self.assertEqual(value.setup('TEST'), 'override')
with patch.dict(os.environ, clear=True, ACME_TEST='override'):
value = Value('default', environ_prefix='ACME_')
self.assertEqual(value.setup('TEST'), 'override')
def test_boolean_values_true(self):
value = BooleanValue(False)
for truthy in value.true_values:
with env(DJANGO_TEST=truthy):
self.assertTrue(bool(value.setup('TEST')))
def test_boolean_values_faulty(self):
self.assertRaises(ValueError, BooleanValue, 'false')
def test_boolean_values_false(self):
value = BooleanValue(True)
for falsy in value.false_values:
with env(DJANGO_TEST=falsy):
self.assertFalse(bool(value.setup('TEST')))
def test_boolean_values_nonboolean(self):
value = BooleanValue(True)
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'):
self.assertEqual(value.setup('TEST'), 2)
with env(DJANGO_TEST='noninteger'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_positive_integer_values(self):
value = PositiveIntegerValue(1)
with env(DJANGO_TEST='2'):
self.assertEqual(value.setup('TEST'), 2)
with env(DJANGO_TEST='noninteger'):
self.assertRaises(ValueError, value.setup, 'TEST')
with env(DJANGO_TEST='-1'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_float_values(self):
value = FloatValue(1.0)
with env(DJANGO_TEST='2.0'):
self.assertEqual(value.setup('TEST'), 2.0)
with env(DJANGO_TEST='noninteger'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_decimal_values(self):
value = DecimalValue(decimal.Decimal(1))
with env(DJANGO_TEST='2'):
self.assertEqual(value.setup('TEST'), decimal.Decimal(2))
with env(DJANGO_TEST='nondecimal'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_failing_caster(self):
self.assertRaises(ImproperlyConfigured, FailingCasterValue)
def test_list_values_default(self):
value = ListValue()
with env(DJANGO_TEST='2,2'):
self.assertEqual(value.setup('TEST'), ['2', '2'])
with env(DJANGO_TEST='2, 2 ,'):
self.assertEqual(value.setup('TEST'), ['2', '2'])
with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), [])
def test_list_values_separator(self):
value = ListValue(separator=':')
with env(DJANGO_TEST='/usr/bin:/usr/sbin:/usr/local/bin'):
self.assertEqual(value.setup('TEST'),
['/usr/bin', '/usr/sbin', '/usr/local/bin'])
def test_List_values_converter(self):
value = ListValue(converter=int)
with env(DJANGO_TEST='2,2'):
self.assertEqual(value.setup('TEST'), [2, 2])
value = ListValue(converter=float)
with env(DJANGO_TEST='2,2'):
self.assertEqual(value.setup('TEST'), [2.0, 2.0])
def test_list_values_custom_converter(self):
value = ListValue(converter=lambda x: x * 2)
with env(DJANGO_TEST='2,2'):
self.assertEqual(value.setup('TEST'), ['22', '22'])
def test_list_values_converter_exception(self):
value = ListValue(converter=int)
with env(DJANGO_TEST='2,b'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_tuple_values_default(self):
value = TupleValue()
with env(DJANGO_TEST='2,2'):
self.assertEqual(value.setup('TEST'), ('2', '2'))
with env(DJANGO_TEST='2, 2 ,'):
self.assertEqual(value.setup('TEST'), ('2', '2'))
with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), ())
def test_single_nested_list_values_default(self):
value = SingleNestedListValue()
with env(DJANGO_TEST='2,3;4,5'):
expected = [['2', '3'], ['4', '5']]
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST='2;3;4;5'):
expected = [['2'], ['3'], ['4'], ['5']]
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST='2,3,4,5'):
expected = [['2', '3', '4', '5']]
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST='2, 3 , ; 4 , 5 ; '):
expected = [['2', '3'], ['4', '5']]
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), [])
def test_single_nested_list_values_separator(self):
value = SingleNestedListValue(seq_separator=':')
with env(DJANGO_TEST='2,3:4,5'):
self.assertEqual(value.setup('TEST'), [['2', '3'], ['4', '5']])
def test_single_nested_list_values_converter(self):
value = SingleNestedListValue(converter=int)
with env(DJANGO_TEST='2,3;4,5'):
self.assertEqual(value.setup('TEST'), [[2, 3], [4, 5]])
def test_single_nested_list_values_converter_default(self):
value = SingleNestedListValue([['2', '3'], ['4', '5']], converter=int)
self.assertEqual(value.value, [[2, 3], [4, 5]])
def test_single_nested_tuple_values_default(self):
value = SingleNestedTupleValue()
with env(DJANGO_TEST='2,3;4,5'):
expected = (('2', '3'), ('4', '5'))
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST='2;3;4;5'):
expected = (('2',), ('3',), ('4',), ('5',))
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST='2,3,4,5'):
expected = (('2', '3', '4', '5'),)
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST='2, 3 , ; 4 , 5 ; '):
expected = (('2', '3'), ('4', '5'))
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), ())
def test_single_nested_tuple_values_separator(self):
value = SingleNestedTupleValue(seq_separator=':')
with env(DJANGO_TEST='2,3:4,5'):
self.assertEqual(value.setup('TEST'), (('2', '3'), ('4', '5')))
def test_single_nested_tuple_values_converter(self):
value = SingleNestedTupleValue(converter=int)
with env(DJANGO_TEST='2,3;4,5'):
self.assertEqual(value.setup('TEST'), ((2, 3), (4, 5)))
def test_single_nested_tuple_values_converter_default(self):
value = SingleNestedTupleValue((('2', '3'), ('4', '5')), converter=int)
self.assertEqual(value.value, ((2, 3), (4, 5)))
def test_set_values_default(self):
value = SetValue()
with env(DJANGO_TEST='2,2'):
self.assertEqual(value.setup('TEST'), {'2', '2'})
with env(DJANGO_TEST='2, 2 ,'):
self.assertEqual(value.setup('TEST'), {'2', '2'})
with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), set())
def test_dict_values_default(self):
value = DictValue()
with env(DJANGO_TEST='{2: 2}'):
self.assertEqual(value.setup('TEST'), {2: 2})
expected = {2: 2, '3': '3', '4': [1, 2, 3]}
with env(DJANGO_TEST="{2: 2, '3': '3', '4': [1, 2, 3]}"):
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST="""{
2: 2,
'3': '3',
'4': [1, 2, 3],
}"""):
self.assertEqual(value.setup('TEST'), expected)
with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), {})
with env(DJANGO_TEST='spam'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_email_values(self):
value = EmailValue('spam@eg.gs')
with env(DJANGO_TEST='spam@sp.am'):
self.assertEqual(value.setup('TEST'), 'spam@sp.am')
with env(DJANGO_TEST='spam'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_url_values(self):
value = URLValue('http://eggs.spam')
with env(DJANGO_TEST='http://spam.eggs'):
self.assertEqual(value.setup('TEST'), 'http://spam.eggs')
with env(DJANGO_TEST='httb://spam.eggs'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_url_values_with_no_default(self):
value = URLValue() # no default
with env(DJANGO_TEST='http://spam.eggs'):
self.assertEqual(value.setup('TEST'), 'http://spam.eggs')
def test_url_values_with_wrong_default(self):
self.assertRaises(ValueError, URLValue, 'httb://spam.eggs')
def test_ip_values(self):
value = IPValue('0.0.0.0')
with env(DJANGO_TEST='127.0.0.1'):
self.assertEqual(value.setup('TEST'), '127.0.0.1')
with env(DJANGO_TEST='::1'):
self.assertEqual(value.setup('TEST'), '::1')
with env(DJANGO_TEST='spam.eggs'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_regex_values(self):
value = RegexValue('000--000', regex=r'\d+--\d+')
with env(DJANGO_TEST='123--456'):
self.assertEqual(value.setup('TEST'), '123--456')
with env(DJANGO_TEST='123456'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_path_values_with_check(self):
value = PathValue()
with env(DJANGO_TEST='/'):
self.assertEqual(value.setup('TEST'), '/')
with env(DJANGO_TEST='~/'):
self.assertEqual(value.setup('TEST'), os.path.expanduser('~'))
with env(DJANGO_TEST='/does/not/exist'):
self.assertRaises(ValueError, value.setup, 'TEST')
def test_path_values_no_check(self):
value = PathValue(check_exists=False)
with env(DJANGO_TEST='/'):
self.assertEqual(value.setup('TEST'), '/')
with env(DJANGO_TEST='~/spam/eggs'):
self.assertEqual(value.setup('TEST'),
os.path.join(os.path.expanduser('~'),
'spam', 'eggs'))
with env(DJANGO_TEST='/does/not/exist'):
self.assertEqual(value.setup('TEST'), '/does/not/exist')
def test_secret_value(self):
# no default allowed, only environment values are
self.assertRaises(ValueError, SecretValue, 'default')
value = SecretValue()
self.assertRaises(ValueError, value.setup, 'TEST')
with env(DJANGO_SECRET_KEY='123'):
self.assertEqual(value.setup('SECRET_KEY'), '123')
value = SecretValue(environ_name='FACEBOOK_API_SECRET',
environ_prefix=None,
late_binding=True)
self.assertRaises(ValueError, value.setup, 'TEST')
with env(FACEBOOK_API_SECRET='123'):
self.assertEqual(value.setup('TEST'), '123')
def test_database_url_value(self):
value = DatabaseURLValue()
self.assertEqual(value.default, {})
with env(DATABASE_URL='sqlite://'):
settings_value = value.setup('DATABASE_URL')
# Compare the embedded dicts in the "default" entry so that the difference can be seen if
# it fails ... DatabaseURLValue(|) uses an external app that can add additional entries
self.assertDictEqual(
{
'CONN_HEALTH_CHECKS': False,
'CONN_MAX_AGE': 0,
'DISABLE_SERVER_SIDE_CURSORS': False,
'ENGINE': 'django.db.backends.sqlite3',
'HOST': '',
'NAME': ':memory:',
'PASSWORD': '',
'PORT': '',
'USER': '',
},
settings_value['default']
)
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, {})
with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:587'): # noqa: E501
self.assertEqual(value.setup('EMAIL_URL'), {
'EMAIL_BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
'EMAIL_FILE_PATH': '',
'EMAIL_HOST': 'smtp.example.com',
'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True})
with env(EMAIL_URL='console://'):
self.assertEqual(value.setup('EMAIL_URL'), {
'EMAIL_BACKEND': 'django.core.mail.backends.console.EmailBackend', # noqa: E501
'EMAIL_FILE_PATH': '',
'EMAIL_HOST': None,
'EMAIL_HOST_PASSWORD': None,
'EMAIL_HOST_USER': None,
'EMAIL_PORT': None,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': False})
with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:wrong'): # noqa: E501
self.assertRaises(ValueError, value.setup, 'TEST')
def test_cache_url_value(self):
cache_setting = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache' if DJANGO_VERSION < (4,) else 'django.core.cache.backends.redis.RedisCache', # noqa: E501
'LOCATION': 'redis://host:6379/1',
}
}
cache_url = 'redis://user@host:6379/1'
value = CacheURLValue(cache_url)
self.assertEqual(value.default, cache_setting)
value = CacheURLValue()
self.assertEqual(value.default, {})
with env(CACHE_URL='redis://user@host:6379/1'):
self.assertEqual(value.setup('CACHE_URL'), cache_setting)
with env(CACHE_URL='wrong://user@host:port/1'):
with self.assertRaises(Exception) as cm:
value.setup('TEST')
self.assertEqual(cm.exception.args[0], 'Unknown backend: "wrong"')
with env(CACHE_URL='redis://user@host:port/1'):
with self.assertRaises(ValueError) as cm:
value.setup('TEST')
self.assertEqual(
cm.exception.args[0],
"Cannot interpret cache URL value 'redis://user@host:port/1'")
def test_search_url_value(self):
value = SearchURLValue()
self.assertEqual(value.default, {})
with env(SEARCH_URL='elasticsearch://127.0.0.1:9200/index'):
self.assertEqual(value.setup('SEARCH_URL'), {
'default': {
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine', # noqa: E501
'URL': 'http://127.0.0.1:9200',
'INDEX_NAME': 'index',
}})
def test_backend_list_value(self):
backends = ['django.middleware.common.CommonMiddleware']
value = BackendsValue(backends)
self.assertEqual(value.setup('TEST'), backends)
backends = ['non.existing.Backend']
self.assertRaises(ValueError, BackendsValue, backends)
def test_tuple_value(self):
value = TupleValue(None)
self.assertEqual(value.default, ())
self.assertEqual(value.value, ())
value = TupleValue((1, 2))
self.assertEqual(value.default, (1, 2))
self.assertEqual(value.value, (1, 2))
def test_set_value(self):
value = SetValue()
self.assertEqual(value.default, set())
self.assertEqual(value.value, set())
value = SetValue([1, 2])
self.assertEqual(value.default, {1, 2})
self.assertEqual(value.value, {1, 2})
def test_setup_value(self):
class Target:
pass
value = EmailURLValue()
with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:587'): # noqa: E501
setup_value(Target, 'EMAIL', value)
self.assertEqual(Target.EMAIL, {
'EMAIL_BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
'EMAIL_FILE_PATH': '',
'EMAIL_HOST': 'smtp.example.com',
'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True
})
self.assertEqual(
Target.EMAIL_BACKEND,
'django.core.mail.backends.smtp.EmailBackend')
self.assertEqual(Target.EMAIL_FILE_PATH, '')
self.assertEqual(Target.EMAIL_HOST, 'smtp.example.com')
self.assertEqual(Target.EMAIL_HOST_PASSWORD, 'password')
self.assertEqual(Target.EMAIL_HOST_USER, 'user@domain.com')
self.assertEqual(Target.EMAIL_PORT, 587)
self.assertEqual(Target.EMAIL_USE_TLS, True)

View file

@ -1,2 +0,0 @@
urlpatterns = [
]

72
tox.ini
View file

@ -1,72 +0,0 @@
[tox]
skipsdist = true
usedevelop = true
minversion = 1.8
envlist =
py311-checkqa
docs
py{39}-dj{32,41,42}
py{310,py310}-dj{32,41,42,50,main}
py{311}-dj{41,42,50,51,main}
py{312}-dj{50,51,main}
py{313}-dj{50,51,main}
[gh-actions]
python =
3.9: py39
3.10: py310
3.11: py311,flake8,readme
3.12: py312
3.13: py313
pypy-3.10: pypy310
[testenv]
usedevelop = true
setenv =
DJANGO_SETTINGS_MODULE = tests.settings.main
DJANGO_CONFIGURATION = Test
COVERAGE_PROCESS_START = {toxinidir}/setup.cfg
deps =
dj32: django~=3.2.9
dj41: django~=4.1.3
dj42: django~=4.2.0
dj50: django~=5.0.0
dj51: django~=5.1.0
djmain: https://github.com/django/django/archive/main.tar.gz
py312: setuptools
py312: wheel
py313: setuptools
py313: wheel
coverage
coverage_enable_subprocess
extras = testing
commands =
python --version
{envbindir}/coverage run {envbindir}/django-cadmin test -v2 {posargs:tests}
coverage combine . tests docs
coverage report -m --skip-covered
coverage xml
[testenv:py311-checkqa]
commands =
flake8 {toxinidir}
check-manifest -v
python setup.py sdist
twine check dist/*
deps =
flake8
twine
check-manifest
[testenv:docs]
setenv =
deps =
-r docs/requirements.txt
commands =
sphinx-build \
-b html \
-a \
-W \
-n \
docs \
docs/_build/html