├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── configurations ├── __init__.py ├── __main__.py ├── asgi.py ├── base.py ├── decorators.py ├── fastcgi.py ├── importer.py ├── management.py ├── sphinx.py ├── utils.py ├── values.py ├── version.py └── wsgi.py ├── docs ├── changes.rst ├── conf.py ├── cookbook.rst ├── index.rst ├── patterns.rst ├── requirements.txt └── values.rst ├── setup.cfg ├── setup.py ├── test_project ├── .env ├── manage.py └── test_project │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── tests ├── __init__.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── dot_env.py │ ├── error.py │ ├── main.py │ ├── mixin_inheritance.py │ ├── multiple_inheritance.py │ └── single_inheritance.py ├── setup_test.py ├── test_env.py ├── test_error.py ├── test_inheritance.py ├── test_main.py ├── test_values.py └── urls.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-configurations' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: 3.x 22 | 23 | - name: Get pip cache dir 24 | id: pip-cache 25 | run: | 26 | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 27 | 28 | - name: Cache 29 | uses: actions/cache@v4 30 | with: 31 | path: ${{ steps.pip-cache.outputs.dir }} 32 | key: release-${{ hashFiles('**/setup.py') }} 33 | restore-keys: | 34 | release- 35 | 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | python -m pip install --upgrade setuptools twine wheel 40 | 41 | - name: Build package 42 | run: | 43 | python setup.py --version 44 | python setup.py sdist --format=gztar bdist_wheel 45 | twine check dist/* 46 | 47 | - name: Upload packages to Jazzband 48 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 49 | uses: pypa/gh-action-pypi-publish@release/v1 50 | with: 51 | user: jazzband 52 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 53 | repository-url: https://jazzband.co/projects/django-configurations/upload 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | max-parallel: 6 15 | matrix: 16 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10'] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Get pip cache dir 27 | id: pip-cache 28 | run: | 29 | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 30 | 31 | - name: Cache 32 | uses: actions/cache@v4 33 | with: 34 | path: ${{ steps.pip-cache.outputs.dir }} 35 | key: 36 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }} 37 | restore-keys: | 38 | ${{ matrix.python-version }}-v1- 39 | 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | python -m pip install --upgrade "tox<4" "tox-gh-actions<3" 44 | 45 | - name: Tox tests 46 | run: | 47 | tox --verbose 48 | 49 | - name: Upload coverage 50 | uses: codecov/codecov-action@v5 51 | with: 52 | name: coverage-data-${{ matrix.python-version }} 53 | path: ".coverage.*" 54 | include-hidden-files: true 55 | merge-multiple: true 56 | fail_ci_if_error: true 57 | token: ${{ secrets.CODECOV_TOKEN }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | coverage.xml 3 | docs/_build 4 | *.egg-info 5 | *.egg 6 | test.db 7 | build/ 8 | .tox/ 9 | htmlcov/ 10 | *.pyc 11 | dist/ 12 | .eggs/ 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: [] 2 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.10" 7 | python: 8 | install: 9 | - requirements: docs/requirements.txt 10 | - method: pip 11 | path: . 12 | sphinx: 13 | configuration: docs/conf.py 14 | formats: 15 | - epub 16 | - pdf 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Arseny Sokolov 2 | Asif Saif Uddin 3 | Baptiste Mispelon 4 | Brian Helba 5 | Bruno Clermont 6 | Christoph Krybus 7 | Finn-Thorben Sell 8 | Gilles Fabio 9 | Jannis Leidel 10 | John Franey 11 | Marc Abramowitz 12 | Michael Käufl 13 | Michael van Tellingen 14 | Mike Fogel 15 | Miro Hrončok 16 | Nicholas Dujay 17 | Paolo Melchiorre 18 | Peter Bittner 19 | Richard de Wit 20 | Thomas Grainger 21 | Tim Gates 22 | Victor Seva 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | 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). 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2023, Jannis Leidel and other contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-configurations nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include .pre-commit-config.yaml 2 | include .readthedocs.yaml 3 | include AUTHORS 4 | include CODE_OF_CONDUCT.md 5 | include CONTRIBUTING.md 6 | include LICENSE 7 | include README.rst 8 | include tox.ini 9 | recursive-include docs * 10 | recursive-include test_project * 11 | recursive-include tests * 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-configurations |latest-version| 2 | ====================================== 3 | 4 | |jazzband| |build-status| |codecov| |docs| |python-support| |django-support| 5 | 6 | django-configurations eases Django project configuration by relying 7 | on the composability of Python classes. It extends the notion of 8 | Django's module based settings loading with well established 9 | object oriented programming patterns. 10 | 11 | Check out the `documentation`_ for more complete examples. 12 | 13 | .. |latest-version| image:: https://img.shields.io/pypi/v/django-configurations.svg 14 | :target: https://pypi.python.org/pypi/django-configurations 15 | :alt: Latest version on PyPI 16 | 17 | .. |jazzband| image:: https://jazzband.co/static/img/badge.svg 18 | :target: https://jazzband.co/ 19 | :alt: Jazzband 20 | 21 | .. |build-status| image:: https://github.com/jazzband/django-configurations/workflows/Test/badge.svg 22 | :target: https://github.com/jazzband/django-configurations/actions 23 | :alt: Build Status 24 | 25 | .. |codecov| image:: https://codecov.io/github/jazzband/django-configurations/coverage.svg?branch=master 26 | :target: https://codecov.io/github/jazzband/django-configurations?branch=master 27 | :alt: Test coverage status 28 | 29 | .. |docs| image:: https://img.shields.io/readthedocs/django-configurations/latest.svg 30 | :target: https://readthedocs.org/projects/django-configurations/ 31 | :alt: Documentation status 32 | 33 | .. |python-support| image:: https://img.shields.io/pypi/pyversions/django-configurations.svg 34 | :target: https://pypi.python.org/pypi/django-configurations 35 | :alt: Supported Python versions 36 | 37 | .. |django-support| image:: https://img.shields.io/pypi/djversions/django-configurations 38 | :target: https://pypi.org/project/django-configurations 39 | :alt: Supported Django versions 40 | 41 | .. _documentation: https://django-configurations.readthedocs.io/en/latest/ 42 | 43 | Quickstart 44 | ---------- 45 | 46 | Install django-configurations: 47 | 48 | .. code-block:: console 49 | 50 | $ python -m pip install django-configurations 51 | 52 | or, alternatively, if you want to use URL-based values: 53 | 54 | .. code-block:: console 55 | 56 | $ python -m pip install django-configurations[cache,database,email,search] 57 | 58 | Then subclass the included ``configurations.Configuration`` class in your 59 | project's **settings.py** or any other module you're using to store the 60 | settings constants, e.g.: 61 | 62 | .. code-block:: python 63 | 64 | # mysite/settings.py 65 | 66 | from configurations import Configuration 67 | 68 | class Dev(Configuration): 69 | DEBUG = True 70 | 71 | Set the ``DJANGO_CONFIGURATION`` environment variable to the name of the class 72 | you just created, e.g. in bash: 73 | 74 | .. code-block:: console 75 | 76 | $ export DJANGO_CONFIGURATION=Dev 77 | 78 | and the ``DJANGO_SETTINGS_MODULE`` environment variable to the module 79 | import path as usual, e.g. in bash: 80 | 81 | .. code-block:: console 82 | 83 | $ export DJANGO_SETTINGS_MODULE=mysite.settings 84 | 85 | *Alternatively* supply the ``--configuration`` option when using Django 86 | management commands along the lines of Django's default ``--settings`` 87 | command line option, e.g. 88 | 89 | .. code-block:: console 90 | 91 | $ python -m manage runserver --settings=mysite.settings --configuration=Dev 92 | 93 | To enable Django to use your configuration you now have to modify your 94 | **manage.py**, **wsgi.py** or **asgi.py** script to use django-configurations's versions 95 | of the appropriate starter functions, e.g. a typical **manage.py** using 96 | django-configurations would look like this: 97 | 98 | .. code-block:: python 99 | 100 | #!/usr/bin/env python 101 | 102 | import os 103 | import sys 104 | 105 | if __name__ == "__main__": 106 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 107 | os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev') 108 | 109 | from configurations.management import execute_from_command_line 110 | 111 | execute_from_command_line(sys.argv) 112 | 113 | Notice in line 10 we don't use the common tool 114 | ``django.core.management.execute_from_command_line`` but instead 115 | ``configurations.management.execute_from_command_line``. 116 | 117 | The same applies to your **wsgi.py** file, e.g.: 118 | 119 | .. code-block:: python 120 | 121 | import os 122 | 123 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 124 | os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev') 125 | 126 | from configurations.wsgi import get_wsgi_application 127 | 128 | application = get_wsgi_application() 129 | 130 | Here we don't use the default ``django.core.wsgi.get_wsgi_application`` 131 | function but instead ``configurations.wsgi.get_wsgi_application``. 132 | 133 | Or if you are not serving your app via WSGI but ASGI instead, you need to modify your **asgi.py** file too.: 134 | 135 | .. code-block:: python 136 | 137 | import os 138 | 139 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 140 | os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev') 141 | 142 | from configurations.asgi import get_asgi_application 143 | 144 | application = get_asgi_application() 145 | 146 | That's it! You can now use your project with ``manage.py`` and your favorite 147 | WSGI/ASGI enabled server. 148 | -------------------------------------------------------------------------------- /configurations/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Configuration # noqa 2 | from .decorators import pristinemethod # noqa 3 | from .version import __version__ # noqa 4 | 5 | 6 | __all__ = ['Configuration', 'pristinemethod'] 7 | 8 | 9 | def _setup(): 10 | from . import importer 11 | 12 | importer.install() 13 | 14 | from django.apps import apps 15 | if not apps.ready: 16 | import django 17 | django.setup() 18 | 19 | 20 | def load_ipython_extension(ipython): 21 | """ 22 | The `ipython` argument is the currently active `InteractiveShell` 23 | instance, which can be used in any way. This allows you to register 24 | new magics or aliases, for example. 25 | """ 26 | _setup() 27 | 28 | 29 | def setup(app=None): 30 | """Function used to initialize configurations similar to :func:`.django.setup`.""" 31 | _setup() 32 | -------------------------------------------------------------------------------- /configurations/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | invokes django-cadmin when the configurations module is run as a script. 3 | 4 | Example: python -m configurations check 5 | """ 6 | 7 | from .management import execute_from_command_line 8 | 9 | if __name__ == "__main__": 10 | execute_from_command_line() 11 | -------------------------------------------------------------------------------- /configurations/asgi.py: -------------------------------------------------------------------------------- 1 | from . import importer 2 | 3 | importer.install() 4 | 5 | from django.core.asgi import get_asgi_application # noqa: E402 6 | 7 | # this is just for the crazy ones 8 | application = get_asgi_application() 9 | -------------------------------------------------------------------------------- /configurations/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from django.conf import global_settings 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | from .utils import uppercase_attributes 8 | from .values import Value, setup_value 9 | 10 | __all__ = ['Configuration'] 11 | 12 | 13 | install_failure = ("django-configurations settings importer wasn't " 14 | "correctly installed. Please use one of the starter " 15 | "functions to install it as mentioned in the docs: " 16 | "https://django-configurations.readthedocs.io/") 17 | 18 | 19 | class ConfigurationBase(type): 20 | 21 | def __new__(cls, name, bases, attrs): 22 | if bases not in ((object,), ()) and bases[0].__name__ != 'NewBase': 23 | # if this is actually a subclass in a settings module 24 | # we better check if the importer was correctly installed 25 | from . import importer 26 | if not importer.installed: 27 | raise ImproperlyConfigured(install_failure) 28 | settings_vars = uppercase_attributes(global_settings) 29 | parents = [base for base in bases if isinstance(base, 30 | ConfigurationBase)] 31 | if parents: 32 | for base in bases[::-1]: 33 | settings_vars.update(uppercase_attributes(base)) 34 | 35 | deprecated_settings = { 36 | # DEFAULT_HASHING_ALGORITHM is always deprecated, as it's a 37 | # transitional setting 38 | # https://docs.djangoproject.com/en/3.1/releases/3.1/#default-hashing-algorithm-settings 39 | "DEFAULT_HASHING_ALGORITHM", 40 | # DEFAULT_CONTENT_TYPE and FILE_CHARSET are deprecated in 41 | # Django 2.2 and are removed in Django 3.0 42 | "DEFAULT_CONTENT_TYPE", 43 | "FILE_CHARSET", 44 | # When DEFAULT_AUTO_FIELD is not explicitly set, Django's emits a 45 | # system check warning models.W042. This warning should not be 46 | # suppressed, as downstream users are expected to make a decision. 47 | # https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys 48 | "DEFAULT_AUTO_FIELD", 49 | # FORMS_URLFIELD_ASSUME_HTTPS is a transitional setting introduced 50 | # in Django 5.0. 51 | # https://docs.djangoproject.com/en/5.0/releases/5.0/#id2 52 | "FORMS_URLFIELD_ASSUME_HTTPS" 53 | } 54 | # PASSWORD_RESET_TIMEOUT_DAYS is deprecated in favor of 55 | # PASSWORD_RESET_TIMEOUT in Django 3.1 56 | # https://github.com/django/django/commit/226ebb17290b604ef29e82fb5c1fbac3594ac163#diff-ec2bed07bb264cb95a80f08d71a47c06R163-R170 57 | if "PASSWORD_RESET_TIMEOUT" in settings_vars: 58 | deprecated_settings.add("PASSWORD_RESET_TIMEOUT_DAYS") 59 | # DEFAULT_FILE_STORAGE and STATICFILES_STORAGE are deprecated 60 | # in favor of STORAGES. 61 | # https://docs.djangoproject.com/en/dev/releases/4.2/#custom-file-storages 62 | if "STORAGES" in settings_vars: 63 | deprecated_settings.add("DEFAULT_FILE_STORAGE") 64 | deprecated_settings.add("STATICFILES_STORAGE") 65 | for deprecated_setting in deprecated_settings: 66 | if deprecated_setting in settings_vars: 67 | del settings_vars[deprecated_setting] 68 | attrs = {**settings_vars, **attrs} 69 | 70 | return super().__new__(cls, name, bases, attrs) 71 | 72 | def __repr__(self): 73 | return "".format(self.__module__, 74 | self.__name__) 75 | 76 | 77 | class Configuration(metaclass=ConfigurationBase): 78 | """ 79 | The base configuration class to inherit from. 80 | 81 | :: 82 | 83 | class Develop(Configuration): 84 | EXTRA_AWESOME = True 85 | 86 | @property 87 | def SOMETHING(self): 88 | return completely.different() 89 | 90 | def OTHER(self): 91 | if whatever: 92 | return (1, 2, 3) 93 | return (4, 5, 6) 94 | 95 | The module this configuration class is located in will 96 | automatically get the class and instance level attributes 97 | with upper characters if the ``DJANGO_CONFIGURATION`` is set 98 | to the name of the class. 99 | 100 | """ 101 | DOTENV_LOADED = None 102 | 103 | @classmethod 104 | def load_dotenv(cls): 105 | """ 106 | Pulled from Honcho code with minor updates, reads local default 107 | environment variables from a .env file located in the project root 108 | or provided directory. 109 | 110 | https://wellfire.co/learn/easier-12-factor-django/ 111 | https://gist.github.com/bennylope/2999704 112 | """ 113 | # check if the class has DOTENV set whether with a path or None 114 | dotenv = getattr(cls, 'DOTENV', None) 115 | 116 | # if DOTENV is falsy we want to disable it 117 | if not dotenv: 118 | return 119 | 120 | # now check if we can access the file since we know we really want to 121 | try: 122 | with open(dotenv) as f: 123 | content = f.read() 124 | except OSError as e: 125 | raise ImproperlyConfigured("Couldn't read .env file " 126 | "with the path {}. Error: " 127 | "{}".format(dotenv, e)) from e 128 | else: 129 | for line in content.splitlines(): 130 | m1 = re.match(r'\A([A-Za-z_0-9]+)=(.*)\Z', line) 131 | if not m1: 132 | continue 133 | key, val = m1.group(1), m1.group(2) 134 | m2 = re.match(r"\A'(.*)'\Z", val) 135 | if m2: 136 | val = m2.group(1) 137 | m3 = re.match(r'\A"(.*)"\Z', val) 138 | if m3: 139 | val = re.sub(r'\\(.)', r'\1', m3.group(1)) 140 | os.environ.setdefault(key, val) 141 | 142 | cls.DOTENV_LOADED = dotenv 143 | 144 | @classmethod 145 | def pre_setup(cls): 146 | if cls.DOTENV_LOADED is None: 147 | cls.load_dotenv() 148 | 149 | @classmethod 150 | def post_setup(cls): 151 | pass 152 | 153 | @classmethod 154 | def setup(cls): 155 | for name, value in uppercase_attributes(cls).items(): 156 | if isinstance(value, Value): 157 | setup_value(cls, name, value) 158 | -------------------------------------------------------------------------------- /configurations/decorators.py: -------------------------------------------------------------------------------- 1 | def pristinemethod(func): 2 | """ 3 | A decorator for handling pristine settings like callables. 4 | 5 | Use it like this:: 6 | 7 | from configurations import Configuration, pristinemethod 8 | 9 | class Develop(Configuration): 10 | 11 | @pristinemethod 12 | def USER_CHECK(user): 13 | return user.check_perms() 14 | 15 | GROUP_CHECK = pristinemethod(lambda user: user.has_group_access()) 16 | 17 | """ 18 | func.pristine = True 19 | return staticmethod(func) 20 | -------------------------------------------------------------------------------- /configurations/fastcgi.py: -------------------------------------------------------------------------------- 1 | from . import importer 2 | 3 | importer.install() 4 | 5 | from django.core.servers.fastcgi import runfastcgi # noqa 6 | -------------------------------------------------------------------------------- /configurations/importer.py: -------------------------------------------------------------------------------- 1 | from importlib.machinery import PathFinder 2 | import logging 3 | import os 4 | import sys 5 | from optparse import OptionParser, make_option 6 | 7 | from django.conf import ENVIRONMENT_VARIABLE as SETTINGS_ENVIRONMENT_VARIABLE 8 | from django.core.exceptions import ImproperlyConfigured 9 | from django.core.management import base 10 | 11 | from .utils import uppercase_attributes, reraise 12 | from .values import Value, setup_value 13 | 14 | installed = False 15 | 16 | CONFIGURATION_ENVIRONMENT_VARIABLE = 'DJANGO_CONFIGURATION' 17 | CONFIGURATION_ARGUMENT = '--configuration' 18 | CONFIGURATION_ARGUMENT_HELP = ('The name of the configuration class to load, ' 19 | 'e.g. "Development". If this isn\'t provided, ' 20 | 'the DJANGO_CONFIGURATION environment ' 21 | 'variable will be used.') 22 | 23 | 24 | configuration_options = (make_option(CONFIGURATION_ARGUMENT, 25 | help=CONFIGURATION_ARGUMENT_HELP),) 26 | 27 | 28 | def install(check_options=False): 29 | global installed 30 | if not installed: 31 | orig_create_parser = base.BaseCommand.create_parser 32 | 33 | def create_parser(self, prog_name, subcommand): 34 | parser = orig_create_parser(self, prog_name, subcommand) 35 | if isinstance(parser, OptionParser): 36 | # in case the option_list is set the create_parser 37 | # will actually return a OptionParser for backward 38 | # compatibility. In that case we should tack our 39 | # options on to the end of the parser on the way out. 40 | for option in configuration_options: 41 | parser.add_option(option) 42 | else: 43 | # probably argparse, let's not import argparse though 44 | parser.add_argument(CONFIGURATION_ARGUMENT, 45 | help=CONFIGURATION_ARGUMENT_HELP) 46 | return parser 47 | 48 | base.BaseCommand.create_parser = create_parser 49 | importer = ConfigurationFinder(check_options=check_options) 50 | sys.meta_path.insert(0, importer) 51 | installed = True 52 | 53 | 54 | class ConfigurationFinder(PathFinder): 55 | modvar = SETTINGS_ENVIRONMENT_VARIABLE 56 | namevar = CONFIGURATION_ENVIRONMENT_VARIABLE 57 | error_msg = ("Configuration cannot be imported, " 58 | "environment variable {0} is undefined.") 59 | 60 | def __init__(self, check_options=False): 61 | self.argv = sys.argv[:] 62 | self.logger = logging.getLogger(__name__) 63 | self.logger.setLevel(logging.DEBUG) 64 | handler = logging.StreamHandler() 65 | self.logger.addHandler(handler) 66 | if check_options: 67 | self.check_options() 68 | self.validate() 69 | if check_options: 70 | self.announce() 71 | 72 | def __repr__(self): 73 | return "".format(self.module, 74 | self.name) 75 | 76 | @property 77 | def module(self): 78 | return os.environ.get(self.modvar) 79 | 80 | @property 81 | def name(self): 82 | return os.environ.get(self.namevar) 83 | 84 | def check_options(self): 85 | parser = base.CommandParser( 86 | usage="%(prog)s subcommand [options] [args]", 87 | add_help=False, 88 | ) 89 | parser.add_argument('--settings') 90 | parser.add_argument('--pythonpath') 91 | parser.add_argument(CONFIGURATION_ARGUMENT, 92 | help=CONFIGURATION_ARGUMENT_HELP) 93 | 94 | parser.add_argument('args', nargs='*') # catch-all 95 | try: 96 | options, args = parser.parse_known_args(self.argv[2:]) 97 | if options.configuration: 98 | os.environ[self.namevar] = options.configuration 99 | base.handle_default_options(options) 100 | except base.CommandError: 101 | pass # Ignore any option errors at this point. 102 | 103 | def validate(self): 104 | if self.name is None: 105 | raise ImproperlyConfigured(self.error_msg.format(self.namevar)) 106 | if self.module is None: 107 | raise ImproperlyConfigured(self.error_msg.format(self.modvar)) 108 | 109 | def announce(self): 110 | if len(self.argv) > 1: 111 | from . import __version__ 112 | from django.utils.termcolors import colorize 113 | from django.core.management.color import no_style 114 | 115 | if '--no-color' in self.argv: 116 | stylize = no_style() 117 | else: 118 | def stylize(text): 119 | return colorize(text, fg='green') 120 | 121 | if (self.argv[1] == 'runserver' 122 | and os.environ.get('RUN_MAIN') == 'true'): 123 | 124 | message = ("django-configurations version {}, using " 125 | "configuration {}".format(__version__ or "", 126 | self.name)) 127 | self.logger.debug(stylize(message)) 128 | 129 | def find_spec(self, fullname, path=None, target=None): 130 | if fullname is not None and fullname == self.module: 131 | spec = super().find_spec(fullname, path, target) 132 | if spec is not None: 133 | wrap_loader(spec.loader, self.name) 134 | return spec 135 | else: 136 | return None 137 | 138 | 139 | def wrap_loader(loader, class_name): 140 | class ConfigurationLoader(loader.__class__): 141 | def exec_module(self, module): 142 | super().exec_module(module) 143 | 144 | mod = module 145 | 146 | cls_path = f'{mod.__name__}.{class_name}' 147 | 148 | try: 149 | cls = getattr(mod, class_name) 150 | except AttributeError as err: # pragma: no cover 151 | reraise( 152 | err, 153 | ( 154 | f"Couldn't find configuration '{class_name}' in " 155 | f"module '{mod.__package__}'" 156 | ), 157 | ) 158 | try: 159 | cls.pre_setup() 160 | cls.setup() 161 | obj = cls() 162 | attributes = uppercase_attributes(obj).items() 163 | for name, value in attributes: 164 | if callable(value) and not getattr(value, 'pristine', False): 165 | value = value() 166 | # in case a method returns a Value instance we have 167 | # to do the same as the Configuration.setup method 168 | if isinstance(value, Value): 169 | setup_value(mod, name, value) 170 | continue 171 | setattr(mod, name, value) 172 | 173 | setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(module.__name__, 174 | class_name)) 175 | cls.post_setup() 176 | 177 | except Exception as err: 178 | reraise(err, f"Couldn't setup configuration '{cls_path}'") 179 | 180 | loader.__class__ = ConfigurationLoader 181 | -------------------------------------------------------------------------------- /configurations/management.py: -------------------------------------------------------------------------------- 1 | from . import importer 2 | 3 | importer.install(check_options=True) 4 | 5 | from django.core.management import (execute_from_command_line, # noqa 6 | call_command) 7 | -------------------------------------------------------------------------------- /configurations/sphinx.py: -------------------------------------------------------------------------------- 1 | from . import _setup, __version__ 2 | 3 | 4 | def setup(app=None): 5 | """ 6 | The callback for Sphinx that acts as a Sphinx extension. 7 | 8 | Add ``'configurations'`` to the ``extensions`` config variable 9 | in your docs' ``conf.py``. 10 | """ 11 | _setup() 12 | return {'version': __version__, 'parallel_read_safe': True} 13 | -------------------------------------------------------------------------------- /configurations/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import sys 3 | import warnings 4 | 5 | from functools import partial 6 | from importlib import import_module 7 | 8 | from django.core.exceptions import ImproperlyConfigured 9 | 10 | 11 | def isuppercase(name): 12 | return name == name.upper() and not name.startswith('_') 13 | 14 | 15 | def uppercase_attributes(obj): 16 | return {name: getattr(obj, name) for name in dir(obj) if isuppercase(name)} 17 | 18 | 19 | def import_by_path(dotted_path, error_prefix=''): 20 | """ 21 | Import a dotted module path and return the attribute/class designated by 22 | the last name in the path. Raise ImproperlyConfigured if something goes 23 | wrong. 24 | 25 | Backported from Django 1.6. 26 | """ 27 | warnings.warn("Function utils.import_by_path is deprecated in favor of " 28 | "django.utils.module_loading.import_string.", DeprecationWarning) 29 | try: 30 | module_path, class_name = dotted_path.rsplit('.', 1) 31 | except ValueError: 32 | raise ImproperlyConfigured("{}{} doesn't look like " 33 | "a module path".format(error_prefix, 34 | dotted_path)) 35 | try: 36 | module = import_module(module_path) 37 | except ImportError as err: 38 | msg = '{}Error importing module {}: "{}"'.format(error_prefix, 39 | module_path, 40 | err) 41 | raise ImproperlyConfigured(msg).with_traceback(sys.exc_info()[2]) 42 | try: 43 | attr = getattr(module, class_name) 44 | except AttributeError: 45 | raise ImproperlyConfigured('{}Module "{}" does not define a ' 46 | '"{}" attribute/class'.format(error_prefix, 47 | module_path, 48 | class_name)) 49 | return attr 50 | 51 | 52 | def reraise(exc, prefix=None, suffix=None): 53 | args = exc.args 54 | if not args: 55 | args = ('',) 56 | if prefix is None: 57 | prefix = '' 58 | elif not prefix.endswith((':', ': ')): 59 | prefix = prefix + ': ' 60 | if suffix is None: 61 | suffix = '' 62 | elif not (suffix.startswith('(') and suffix.endswith(')')): 63 | suffix = '(' + suffix + ')' 64 | exc.args = (f'{prefix} {args[0]} {suffix}',) + args[1:] 65 | raise exc 66 | 67 | 68 | # Copied over from Sphinx 69 | def getargspec(func): 70 | """Like inspect.getargspec but supports functools.partial as well.""" 71 | if inspect.ismethod(func): 72 | func = func.__func__ 73 | if type(func) is partial: 74 | orig_func = func.func 75 | argspec = getargspec(orig_func) 76 | args = list(argspec[0]) 77 | defaults = list(argspec[3] or ()) 78 | kwoargs = list(argspec[4]) 79 | kwodefs = dict(argspec[5] or {}) 80 | if func.args: 81 | args = args[len(func.args):] 82 | for arg in func.keywords or (): 83 | try: 84 | i = args.index(arg) - len(args) 85 | del args[i] 86 | try: 87 | del defaults[i] 88 | except IndexError: 89 | pass 90 | except ValueError: # must be a kwonly arg 91 | i = kwoargs.index(arg) 92 | del kwoargs[i] 93 | del kwodefs[arg] 94 | return inspect.FullArgSpec(args, argspec[1], argspec[2], 95 | tuple(defaults), kwoargs, 96 | kwodefs, argspec[6]) 97 | while hasattr(func, '__wrapped__'): 98 | func = func.__wrapped__ 99 | if not inspect.isfunction(func): 100 | raise TypeError('%r is not a Python function' % func) 101 | return inspect.getfullargspec(func) 102 | -------------------------------------------------------------------------------- /configurations/values.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import copy 3 | import decimal 4 | import os 5 | import sys 6 | 7 | from django.core import validators 8 | from django.core.exceptions import ValidationError, ImproperlyConfigured 9 | from django.utils.module_loading import import_string 10 | 11 | from .utils import getargspec 12 | 13 | 14 | def setup_value(target, name, value): 15 | actual_value = value.setup(name) 16 | # overwriting the original Value class with the result 17 | setattr(target, name, value.value) 18 | if value.multiple: 19 | for multiple_name, multiple_value in actual_value.items(): 20 | setattr(target, multiple_name, multiple_value) 21 | 22 | 23 | class Value: 24 | """ 25 | A single settings value that is able to interpret env variables 26 | and implements a simple validation scheme. 27 | """ 28 | multiple = False 29 | late_binding = False 30 | environ_required = False 31 | 32 | @property 33 | def value(self): 34 | value = self.default 35 | if not hasattr(self, '_value') and self.environ_name: 36 | self.setup(self.environ_name) 37 | if hasattr(self, '_value'): 38 | value = self._value 39 | return value 40 | 41 | @value.setter 42 | def value(self, value): 43 | self._value = value 44 | 45 | def __new__(cls, *args, **kwargs): 46 | """ 47 | checks if the creation can end up directly in the final value. 48 | That is the case whenever environ = False or environ_name is given. 49 | """ 50 | instance = object.__new__(cls) 51 | if 'late_binding' in kwargs: 52 | instance.late_binding = kwargs.get('late_binding') 53 | if not instance.late_binding: 54 | instance.__init__(*args, **kwargs) 55 | if ((instance.environ and instance.environ_name) 56 | or (not instance.environ and instance.default)): 57 | instance = instance.setup(instance.environ_name) 58 | return instance 59 | 60 | def __init__(self, default=None, environ=True, environ_name=None, 61 | environ_prefix='DJANGO', environ_required=False, 62 | *args, **kwargs): 63 | if isinstance(default, Value) and default.default is not None: 64 | self.default = copy.copy(default.default) 65 | else: 66 | self.default = default 67 | self.environ = environ 68 | if environ_prefix and environ_prefix.endswith('_'): 69 | environ_prefix = environ_prefix[:-1] 70 | self.environ_prefix = environ_prefix 71 | self.environ_name = environ_name 72 | self.environ_required = environ_required 73 | 74 | def __str__(self): 75 | return str(self.value) 76 | 77 | def __repr__(self): 78 | return repr(self.value) 79 | 80 | def __eq__(self, other): 81 | return self.value == other 82 | 83 | def __bool__(self): 84 | return bool(self.value) 85 | 86 | # Compatibility with python 2 87 | __nonzero__ = __bool__ 88 | 89 | def full_environ_name(self, name): 90 | if self.environ_name: 91 | environ_name = self.environ_name 92 | else: 93 | environ_name = name.upper() 94 | if self.environ_prefix: 95 | environ_name = f'{self.environ_prefix}_{environ_name}' 96 | return environ_name 97 | 98 | def setup(self, name): 99 | value = self.default 100 | if self.environ: 101 | full_environ_name = self.full_environ_name(name) 102 | if full_environ_name in os.environ: 103 | value = self.to_python(os.environ[full_environ_name]) 104 | elif self.environ_required: 105 | raise ValueError('Value {!r} is required to be set as the ' 106 | 'environment variable {!r}' 107 | .format(name, full_environ_name)) 108 | self.value = value 109 | return value 110 | 111 | def to_python(self, value): 112 | """ 113 | Convert the given value of a environment variable into an 114 | appropriate Python representation of the value. 115 | This should be overridden when subclassing. 116 | """ 117 | return value 118 | 119 | 120 | class MultipleMixin: 121 | multiple = True 122 | 123 | 124 | class BooleanValue(Value): 125 | true_values = ('yes', 'y', 'true', '1') 126 | false_values = ('no', 'n', 'false', '0', '') 127 | 128 | def __init__(self, *args, **kwargs): 129 | super().__init__(*args, **kwargs) 130 | if self.default not in (True, False): 131 | raise ValueError('Default value {!r} is not a ' 132 | 'boolean value'.format(self.default)) 133 | 134 | def to_python(self, value): 135 | normalized_value = value.strip().lower() 136 | if normalized_value in self.true_values: 137 | return True 138 | elif normalized_value in self.false_values: 139 | return False 140 | else: 141 | raise ValueError('Cannot interpret ' 142 | 'boolean value {!r}'.format(value)) 143 | 144 | 145 | class CastingMixin: 146 | exception = (TypeError, ValueError) 147 | message = 'Cannot interpret value {0!r}' 148 | 149 | def __init__(self, *args, **kwargs): 150 | super().__init__(*args, **kwargs) 151 | if isinstance(self.caster, str): 152 | try: 153 | self._caster = import_string(self.caster) 154 | except ImportError as err: 155 | msg = f"Could not import {self.caster!r}" 156 | raise ImproperlyConfigured(msg) from err 157 | elif callable(self.caster): 158 | self._caster = self.caster 159 | else: 160 | error = 'Cannot use caster of {} ({!r})'.format(self, 161 | self.caster) 162 | raise ValueError(error) 163 | try: 164 | arg_names = getargspec(self._caster)[0] 165 | self._params = {name: kwargs[name] for name in arg_names if name in kwargs} 166 | except TypeError: 167 | self._params = {} 168 | 169 | def to_python(self, value): 170 | try: 171 | if self._params: 172 | return self._caster(value, **self._params) 173 | else: 174 | return self._caster(value) 175 | except self.exception: 176 | raise ValueError(self.message.format(value)) 177 | 178 | 179 | class IntegerValue(CastingMixin, Value): 180 | caster = int 181 | 182 | 183 | class PositiveIntegerValue(IntegerValue): 184 | 185 | def to_python(self, value): 186 | int_value = super().to_python(value) 187 | if int_value < 0: 188 | raise ValueError(self.message.format(value)) 189 | return int_value 190 | 191 | 192 | class FloatValue(CastingMixin, Value): 193 | caster = float 194 | 195 | 196 | class DecimalValue(CastingMixin, Value): 197 | caster = decimal.Decimal 198 | exception = decimal.InvalidOperation 199 | 200 | 201 | class SequenceValue(Value): 202 | """ 203 | Common code for sequence-type values (lists and tuples). 204 | Do not use this class directly. Instead use a subclass. 205 | """ 206 | 207 | # Specify this value in subclasses, e.g. with 'list' or 'tuple' 208 | sequence_type = None 209 | converter = None 210 | 211 | def __init__(self, *args, **kwargs): 212 | msg = 'Cannot interpret {0} item {{0!r}} in {0} {{1!r}}' 213 | self.message = msg.format(self.sequence_type.__name__) 214 | self.separator = kwargs.pop('separator', ',') 215 | converter = kwargs.pop('converter', None) 216 | if converter is not None: 217 | self.converter = converter 218 | super().__init__(*args, **kwargs) 219 | # make sure the default is the correct sequence type 220 | if self.default is None: 221 | self.default = self.sequence_type() 222 | else: 223 | self.default = self.sequence_type(self.default) 224 | # initial conversion 225 | if self.converter is not None: 226 | self.default = self._convert(self.default) 227 | 228 | def _convert(self, sequence): 229 | converted_values = [] 230 | for value in sequence: 231 | try: 232 | converted_values.append(self.converter(value)) 233 | except (TypeError, ValueError): 234 | raise ValueError(self.message.format(value, value)) 235 | return self.sequence_type(converted_values) 236 | 237 | def to_python(self, value): 238 | split_value = [v.strip() for v in value.strip().split(self.separator)] 239 | # removing empty items 240 | value_list = self.sequence_type(filter(None, split_value)) 241 | if self.converter is not None: 242 | value_list = self._convert(value_list) 243 | return self.sequence_type(value_list) 244 | 245 | 246 | class ListValue(SequenceValue): 247 | sequence_type = list 248 | 249 | 250 | class TupleValue(SequenceValue): 251 | sequence_type = tuple 252 | 253 | 254 | class SingleNestedSequenceValue(SequenceValue): 255 | """ 256 | Common code for nested sequences (list of lists, or tuple of tuples). 257 | Do not use this class directly. Instead use a subclass. 258 | """ 259 | 260 | def __init__(self, *args, **kwargs): 261 | self.seq_separator = kwargs.pop('seq_separator', ';') 262 | super().__init__(*args, **kwargs) 263 | 264 | def _convert(self, items): 265 | # This could receive either a bare or nested sequence 266 | if items and isinstance(items[0], self.sequence_type): 267 | converted_sequences = [ 268 | super(SingleNestedSequenceValue, self)._convert(i) for i in items 269 | ] 270 | return self.sequence_type(converted_sequences) 271 | return self.sequence_type(super()._convert(items)) 272 | 273 | def to_python(self, value): 274 | split_value = [ 275 | v.strip() for v in value.strip().split(self.seq_separator) 276 | ] 277 | # Remove empty items 278 | filtered = self.sequence_type(filter(None, split_value)) 279 | sequence = [ 280 | super(SingleNestedSequenceValue, self).to_python(f) for f in filtered 281 | ] 282 | return self.sequence_type(sequence) 283 | 284 | 285 | class SingleNestedListValue(SingleNestedSequenceValue): 286 | sequence_type = list 287 | 288 | 289 | class SingleNestedTupleValue(SingleNestedSequenceValue): 290 | sequence_type = tuple 291 | 292 | 293 | class BackendsValue(ListValue): 294 | 295 | def converter(self, value): 296 | try: 297 | import_string(value) 298 | except ImportError as err: 299 | raise ValueError(err).with_traceback(sys.exc_info()[2]) 300 | return value 301 | 302 | 303 | class SetValue(ListValue): 304 | message = 'Cannot interpret set item {0!r} in set {1!r}' 305 | 306 | def __init__(self, *args, **kwargs): 307 | super().__init__(*args, **kwargs) 308 | if self.default is None: 309 | self.default = set() 310 | else: 311 | self.default = set(self.default) 312 | 313 | def to_python(self, value): 314 | return set(super().to_python(value)) 315 | 316 | 317 | class DictValue(Value): 318 | message = 'Cannot interpret dict value {0!r}' 319 | 320 | def __init__(self, *args, **kwargs): 321 | super().__init__(*args, **kwargs) 322 | if self.default is None: 323 | self.default = {} 324 | else: 325 | self.default = dict(self.default) 326 | 327 | def to_python(self, value): 328 | value = super().to_python(value) 329 | if not value: 330 | return {} 331 | try: 332 | evaled_value = ast.literal_eval(value) 333 | except ValueError: 334 | raise ValueError(self.message.format(value)) 335 | if not isinstance(evaled_value, dict): 336 | raise ValueError(self.message.format(value)) 337 | return evaled_value 338 | 339 | 340 | class ValidationMixin: 341 | 342 | def __init__(self, *args, **kwargs): 343 | super().__init__(*args, **kwargs) 344 | if isinstance(self.validator, str): 345 | try: 346 | self._validator = import_string(self.validator) 347 | except ImportError as err: 348 | msg = f"Could not import {self.validator!r}" 349 | raise ImproperlyConfigured(msg) from err 350 | elif callable(self.validator): 351 | self._validator = self.validator 352 | else: 353 | raise ValueError('Cannot use validator of ' 354 | '{} ({!r})'.format(self, self.validator)) 355 | if self.default: 356 | self.to_python(self.default) 357 | 358 | def to_python(self, value): 359 | try: 360 | self._validator(value) 361 | except ValidationError: 362 | raise ValueError(self.message.format(value)) 363 | else: 364 | return value 365 | 366 | 367 | class EmailValue(ValidationMixin, Value): 368 | message = 'Cannot interpret email value {0!r}' 369 | validator = 'django.core.validators.validate_email' 370 | 371 | 372 | class URLValue(ValidationMixin, Value): 373 | message = 'Cannot interpret URL value {0!r}' 374 | validator = validators.URLValidator() 375 | 376 | 377 | class IPValue(ValidationMixin, Value): 378 | message = 'Cannot interpret IP value {0!r}' 379 | validator = 'django.core.validators.validate_ipv46_address' 380 | 381 | 382 | class RegexValue(ValidationMixin, Value): 383 | message = "Regex doesn't match value {0!r}" 384 | 385 | def __init__(self, *args, **kwargs): 386 | regex = kwargs.pop('regex', None) 387 | self.validator = validators.RegexValidator(regex=regex) 388 | super().__init__(*args, **kwargs) 389 | 390 | 391 | class PathValue(Value): 392 | def __init__(self, *args, **kwargs): 393 | self.check_exists = kwargs.pop('check_exists', True) 394 | super().__init__(*args, **kwargs) 395 | 396 | def setup(self, name): 397 | value = super().setup(name) 398 | value = os.path.expanduser(value) 399 | if self.check_exists and not os.path.exists(value): 400 | raise ValueError(f'Path {value!r} does not exist.') 401 | return os.path.abspath(value) 402 | 403 | 404 | class SecretValue(Value): 405 | 406 | def __init__(self, *args, **kwargs): 407 | kwargs['environ'] = True 408 | kwargs['environ_required'] = True 409 | super().__init__(*args, **kwargs) 410 | if self.default is not None: 411 | raise ValueError('Secret values are only allowed to ' 412 | 'be set as environment variables') 413 | 414 | def setup(self, name): 415 | value = super().setup(name) 416 | if not value: 417 | raise ValueError(f'Secret value {name!r} is not set') 418 | return value 419 | 420 | 421 | class EmailURLValue(CastingMixin, MultipleMixin, Value): 422 | caster = 'dj_email_url.parse' 423 | message = 'Cannot interpret email URL value {0!r}' 424 | late_binding = True 425 | 426 | def __init__(self, *args, **kwargs): 427 | kwargs.setdefault('environ', True) 428 | kwargs.setdefault('environ_prefix', None) 429 | kwargs.setdefault('environ_name', 'EMAIL_URL') 430 | super().__init__(*args, **kwargs) 431 | if self.default is None: 432 | self.default = {} 433 | else: 434 | self.default = self.to_python(self.default) 435 | 436 | 437 | class DictBackendMixin(Value): 438 | default_alias = 'default' 439 | 440 | def __init__(self, *args, **kwargs): 441 | self.alias = kwargs.pop('alias', self.default_alias) 442 | kwargs.setdefault('environ', True) 443 | kwargs.setdefault('environ_prefix', None) 444 | kwargs.setdefault('environ_name', self.environ_name) 445 | super().__init__(*args, **kwargs) 446 | if self.default is None: 447 | self.default = {} 448 | else: 449 | self.default = self.to_python(self.default) 450 | 451 | def to_python(self, value): 452 | value = super().to_python(value) 453 | return {self.alias: value} 454 | 455 | 456 | class DatabaseURLValue(DictBackendMixin, CastingMixin, Value): 457 | caster = 'dj_database_url.parse' 458 | message = 'Cannot interpret database URL value {0!r}' 459 | environ_name = 'DATABASE_URL' 460 | late_binding = True 461 | 462 | 463 | class CacheURLValue(DictBackendMixin, CastingMixin, Value): 464 | caster = 'django_cache_url.parse' 465 | message = 'Cannot interpret cache URL value {0!r}' 466 | environ_name = 'CACHE_URL' 467 | late_binding = True 468 | 469 | 470 | class SearchURLValue(DictBackendMixin, CastingMixin, Value): 471 | caster = 'dj_search_url.parse' 472 | message = 'Cannot interpret Search URL value {0!r}' 473 | environ_name = 'SEARCH_URL' 474 | late_binding = True 475 | -------------------------------------------------------------------------------- /configurations/version.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import PackageNotFoundError, version 2 | 3 | try: 4 | __version__ = version("django-configurations") 5 | except PackageNotFoundError: 6 | # package is not installed 7 | __version__ = None 8 | -------------------------------------------------------------------------------- /configurations/wsgi.py: -------------------------------------------------------------------------------- 1 | from . import importer 2 | 3 | importer.install() 4 | 5 | from django.core.wsgi import get_wsgi_application # noqa: E402 6 | 7 | # this is just for the crazy ones 8 | application = get_wsgi_application() 9 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Changelog 4 | --------- 5 | 6 | Unreleased 7 | ^^^^^^^^^^ 8 | 9 | - Prevent warning about ``FORMS_URLFIELD_ASSUME_HTTPS`` on Django 5.0. 10 | 11 | v2.5.1 (2023-11-30) 12 | ^^^^^^^^^^^^^^^^^^^ 13 | 14 | - Add compatibility with Python 3.12 15 | 16 | v2.5 (2023-10-20) 17 | ^^^^^^^^^^^^^^^^^ 18 | 19 | - Update Github actions and fix pipeline warnings 20 | - Add compatibility with Django 5.0 21 | - **BACKWARD INCOMPATIBLE** Drop compatibility for Django 4.0 22 | - **BACKWARD INCOMPATIBLE** Drop compatibility for Python 3.7 and PyPy < 3.10 23 | 24 | v2.4.2 (2023-09-27) 25 | ^^^^^^^^^^^^^^^^^^^ 26 | 27 | - Replace imp (due for removal in Python 3.12) with importlib 28 | - Test on PyPy 3.10. 29 | 30 | v2.4.1 (2023-04-04) 31 | ^^^^^^^^^^^^^^^^^^^ 32 | 33 | - Use furo as documentation theme 34 | - Add compatibility with Django 4.2 - fix "STATICFILES_STORAGE/STORAGES are mutually exclusive" error. 35 | - Test Django 4.1.3+ on Python 3.11 36 | 37 | v2.4 (2022-08-24) 38 | ^^^^^^^^^^^^^^^^^ 39 | 40 | - Add compatibility with Django 4.1 41 | - **BACKWARD INCOMPATIBLE** Drop compatibility for Django < 3.2 42 | - **BACKWARD INCOMPATIBLE** Drop compatibility for Python 3.6 43 | 44 | v2.3.2 (2022-01-25) 45 | ^^^^^^^^^^^^^^^^^^^ 46 | 47 | - Add compatibility with Django 4.0 48 | - Fix regression where settings receiving a default were ignored. #323 #327 49 | 50 | v2.3.1 (2021-11-08) 51 | ^^^^^^^^^^^^^^^^^^^ 52 | 53 | - Test Django 3.2 on Python 3.10 as well. 54 | 55 | - Test on PyPy 3.6, 3.7 and 3.8. 56 | 57 | - Enforce Python version requirement during installation (>=3.6). 58 | 59 | - Fix and refactor the documentation build process. 60 | 61 | v2.3 (2021-10-27) 62 | ^^^^^^^^^^^^^^^^^ 63 | 64 | - **BACKWARD INCOMPATIBLE** Drop support for Python 2.7 and 3.5. 65 | 66 | - **BACKWARD INCOMPATIBLE** Drop support for Django < 2.2. 67 | 68 | - Add support for Django 3.1 and 3.2. 69 | 70 | - Add suppport for Python 3.9 and 3.10. 71 | 72 | - Deprecate ``utils.import_by_path`` in favor of 73 | ``django.utils.module_loading.import_string``. 74 | 75 | - Add ASGI support. 76 | 77 | - Added "python -m configurations" entry point. 78 | 79 | - Make package ``install_requires`` include ``django>=2.2``. 80 | 81 | - Prevent an ImproperlyConfigured warning from ``DEFAULT_HASHING_ALGORITHM``. 82 | 83 | - Prevent warnings for settings deprecated in Django 2.2 84 | (``DEFAULT_CONTENT_TYPE`` and ``FILE_CHARSET``). 85 | 86 | - Preserve Django warnings when ``DEFAULT_AUTO_FIELD`` is not set. 87 | 88 | - Miscellaneous documentation fixes. 89 | 90 | - Miscellaneous internal improvements. 91 | 92 | v2.2 (2019-12-03) 93 | ^^^^^^^^^^^^^^^^^ 94 | 95 | - **BACKWARD INCOMPATIBLE** Drop support for Python 3.4. 96 | 97 | - **BACKWARD INCOMPATIBLE** Drop support for Django < 1.11. 98 | 99 | - Add support for Django 3.0. 100 | 101 | - Add support for Python 3.8. 102 | 103 | - Add support for PyPy 3. 104 | 105 | - Replace ``django.utils.six`` with ``six`` to support Django >= 3. 106 | 107 | - Start using tox-travis and setuptools-scm for simplified test harness 108 | and release management. 109 | 110 | v2.1 (2018-08-16) 111 | ^^^^^^^^^^^^^^^^^ 112 | 113 | - **BACKWARD INCOMPATIBLE** Drop support of Python 3.3. 114 | 115 | - **BACKWARD INCOMPATIBLE** Drop support of Django 1.9. 116 | 117 | - Add support for Django 2.1. 118 | 119 | - Add ``PositiveIntegerValue`` configuration value. 120 | 121 | - Fix ``bool(BooleanValue)`` to behave as one would expect (e.g. 122 | ``bool(BooleanValue(False))`` returns ``False``). 123 | 124 | - Miscellaneous documentation improvements and bug fixes. 125 | 126 | v2.0 (2016-07-29) 127 | ^^^^^^^^^^^^^^^^^ 128 | 129 | - **BACKWARD INCOMPATIBLE** Drop support of Python 2.6 and 3.2 130 | 131 | - **BACKWARD INCOMPATIBLE** Drop support of Django < 1.8 132 | 133 | - **BACKWARD INCOMPATIBLE** Moved sphinx callable has been moved from 134 | ``configurations`` to ``configurations.sphinx``. 135 | 136 | - **BACKWARD INCOMPATIBLE** Removed the previously deprecated 137 | ``configurations.Settings`` class in favor of the 138 | ``configurations.Configuration`` added in 0.4. This removal was planned for 139 | the 1.0 release and is now finally enacted. 140 | 141 | - Add multiprocessing support for sphinx integration 142 | 143 | - Fix a RemovedInDjango19Warning warning 144 | 145 | v1.0 (2016-01-04) 146 | ^^^^^^^^^^^^^^^^^ 147 | 148 | - Project has moved to `Jazzband `_. See guidelines for 149 | contributing. 150 | 151 | - Support for Django 1.8 and above. 152 | 153 | - Allow ``Value`` classes to be used outside of ``Configuration`` classes. (#62) 154 | 155 | - Fixed "Value with ValidationMixin will raise ValueError if no default assigned". (#69) 156 | 157 | - Fixed wrong behaviour when assigning BooleanValue. (#83) 158 | 159 | - Add ability to programmatically call Django commands from configurations using 160 | ``call_command``. 161 | 162 | - Added SingleNestedTupleValue and SingleNestedListValue classes. (#85) 163 | 164 | - Several other miscellaneous bugfixes. 165 | 166 | v0.8 (2014-01-16) 167 | ^^^^^^^^^^^^^^^^^ 168 | 169 | - Added ``SearchURLValue`` to configure Haystack ``HAYSTACK_CONNECTIONS`` 170 | settings. 171 | 172 | v0.7 (2013-11-26) 173 | ^^^^^^^^^^^^^^^^^ 174 | 175 | - Removed the broken stdout wrapper that displayed the currently enabled 176 | configuration when using the runserver management command. Added a logging 177 | based solution instead. 178 | 179 | - Fixed default value of ``CacheURLValue`` class that was shadowed by an 180 | unneeded name parameter. Thanks to Stefan Wehrmeyer. 181 | 182 | - Fixed command line options checking in the importer to happen before the 183 | validation. Thanks to Stefan Wehrmeyer. 184 | 185 | - Added Tox test configuration. 186 | 187 | - Fixed an erroneous use of ``PathValue`` in the 1.6.x project template. 188 | 189 | v0.6 (2013-09-19) 190 | ^^^^^^^^^^^^^^^^^ 191 | 192 | - Added a IPython extension to support IPython notebooks correctly. See 193 | the :doc:`cookbook` for more information. 194 | 195 | v0.5.1 (2013-09-12) 196 | ^^^^^^^^^^^^^^^^^^^ 197 | 198 | - Prevented accidentally parsing the command line options to look for the 199 | ``--configuration`` option outside of Django's management commands. 200 | This should fix a problem with gunicorn's own ``--config`` option. 201 | Thanks to Brian Rosner for the report. 202 | 203 | v0.5 (2013-09-09) 204 | ^^^^^^^^^^^^^^^^^ 205 | 206 | - Switched from raising Django's ``ImproperlyConfigured`` exception on errors 207 | to standard ``ValueError`` to prevent hiding those errors when Django 208 | specially handles the first. 209 | 210 | - Switched away from d2to1 as a way to define package metadata since distutils2 211 | is dead. 212 | 213 | - Extended ``Value`` class documentation and fixed other issues. 214 | 215 | - Moved tests out of the ``configurations`` package for easier maintenance. 216 | 217 | v0.4 (2013-09-03) 218 | ^^^^^^^^^^^^^^^^^ 219 | 220 | - Added ``Value`` classes and subclasses for easier handling of settings values, 221 | including populating them from environment variables. 222 | 223 | - Renamed ``configurations.Settings`` class to ``configurations.Configuration`` 224 | to better describe what the class is all about. The old class still exists 225 | and is marked as pending deprecation. It'll be removed in version 1.0. 226 | 227 | - Added a ``setup`` method to handle the new ``Value`` classes and allow an 228 | in-between modification of the configuration values. 229 | 230 | - Added Django project templates for 1.5.x and 1.6.x. 231 | 232 | - Reorganized and extended documentation. 233 | 234 | v0.3.2 (2014-01-16) 235 | ^^^^^^^^^^^^^^^^^^^ 236 | 237 | - Fixed an installation issue. 238 | 239 | v0.3.1 (2013-09-20) 240 | ^^^^^^^^^^^^^^^^^^^ 241 | 242 | - Backported a fix from master that makes 0.3.x compatible with newer 243 | versions of six. 244 | 245 | v0.3 (2013-05-15) 246 | ^^^^^^^^^^^^^^^^^ 247 | 248 | - Added ``pristinemethod`` decorator to be able to have callables as settings. 249 | 250 | - Added ``pre_setup`` and ``post_setup`` method hooks to be able to run code 251 | before or after the settings loading is finished. 252 | 253 | - Minor docs and tests cleanup. 254 | 255 | v0.2.1 (2013-04-11) 256 | ^^^^^^^^^^^^^^^^^^^ 257 | 258 | - Fixed a regression in parsing the new ``-C``/``--configuration`` management 259 | command option. 260 | 261 | - Minor fix in showing the configuration in the ``runserver`` management 262 | command output. 263 | 264 | v0.2 (2013-03-27) 265 | ^^^^^^^^^^^^^^^^^ 266 | 267 | - **backward incompatible change** Dropped support for Python 2.5! Please use 268 | the 0.1 version if you really want. 269 | 270 | - Added Python>3.2 and Django 1.5 support! 271 | 272 | - Catch error when getting or evaluating callable setting class attributes. 273 | 274 | - Simplified and extended tests. 275 | 276 | - Added optional ``-C``/``--configuration`` management command option similar 277 | to Django's ``--settings`` option 278 | 279 | - Fixed the runserver message about which setting is used to 280 | show the correct class. 281 | 282 | - Stopped hiding AttributeErrors happening during initialization 283 | of settings classes. 284 | 285 | - Added FastCGI helper. 286 | 287 | - Minor documentation fixes 288 | 289 | v0.1 (2012-07-21) 290 | ^^^^^^^^^^^^^^^^^ 291 | 292 | - Initial public release 293 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import configurations 2 | 3 | # -- Project information ----------------------------------------------------- 4 | project = 'django-configurations' 5 | copyright = '2012-2023, Jannis Leidel and other contributors' 6 | author = 'Jannis Leidel and other contributors' 7 | 8 | release = configurations.__version__ 9 | version = ".".join(release.split(".")[:2]) 10 | 11 | # -- General configuration --------------------------------------------------- 12 | add_function_parentheses = False 13 | add_module_names = False 14 | 15 | extensions = [ 16 | 'sphinx.ext.autodoc', 17 | 'sphinx.ext.intersphinx', 18 | 'sphinx.ext.viewcode', 19 | ] 20 | 21 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 22 | 23 | intersphinx_mapping = { 24 | 'python': ('https://docs.python.org/3', None), 25 | 'sphinx': ('https://www.sphinx-doc.org/en/master', None), 26 | 'django': ('https://docs.djangoproject.com/en/dev', 27 | 'https://docs.djangoproject.com/en/dev/_objects/'), 28 | } 29 | 30 | # -- Options for HTML output ------------------------------------------------- 31 | html_theme = 'furo' 32 | 33 | # -- Options for Epub output --------------------------------------------------- 34 | epub_title = project 35 | epub_author = author 36 | epub_publisher = author 37 | epub_copyright = copyright 38 | 39 | # -- Options for LaTeX output -------------------------------------------------- 40 | latex_documents = [ 41 | # (source start file, target name, title, author, documentclass) 42 | ('index', 'django-configurations.tex', 43 | 'django-configurations Documentation', author, 'manual'), 44 | ] 45 | -------------------------------------------------------------------------------- /docs/cookbook.rst: -------------------------------------------------------------------------------- 1 | Cookbook 2 | ======== 3 | 4 | Calling a Django management command 5 | ----------------------------------- 6 | 7 | .. versionadded:: 0.9 8 | 9 | If you want to call a Django management command programmatically, say 10 | from a script outside of your usual Django code, you can use the 11 | equivalent of Django's :func:`~django.core.management.call_command` 12 | function with django-configurations, too. 13 | 14 | Simply import it from ``configurations.management`` instead: 15 | 16 | .. code-block:: python 17 | :emphasize-lines: 1 18 | 19 | from configurations.management import call_command 20 | 21 | call_command('dumpdata', exclude=['contenttypes', 'auth']) 22 | 23 | Read .env file 24 | -------------- 25 | 26 | Configurations can read values for environment variables out of an ``.env`` 27 | file, and push them into the application's process environment. Simply set 28 | the ``DOTENV`` setting to the appropriate file name: 29 | 30 | .. code-block:: python 31 | 32 | # mysite/settings.py 33 | 34 | import os.path 35 | from configurations import Configuration, values 36 | 37 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 38 | 39 | class Dev(Configuration): 40 | DOTENV = os.path.join(BASE_DIR, '.env') 41 | 42 | SECRET_KEY = values.SecretValue() 43 | API_KEY1 = values.Value() 44 | API_KEY2 = values.Value() 45 | API_KEY3 = values.Value('91011') 46 | 47 | 48 | A ``.env`` file is a ``.ini``-style file. It must contain a list of 49 | ``KEY=value`` pairs, just like Shell environment variables: 50 | 51 | .. code-block:: ini 52 | 53 | # .env 54 | 55 | DJANGO_DEBUG=False 56 | DJANGO_SECRET_KEY=1q2w3e4r5t6z7u8i9o0(%&)$§!pqaycz 57 | API_KEY1=1234 58 | API_KEY2=5678 59 | 60 | Envdir 61 | ------ 62 | 63 | envdir_ is an effective way to set a large number of environment variables 64 | at once during startup of a command. This is great in combination with 65 | django-configuration's :class:`~configurations.values.Value` subclasses 66 | when enabling their ability to check environment variables for override 67 | values. 68 | 69 | Imagine for example you want to set a few environment variables, all you 70 | have to do is to create a directory with files that have capitalized names 71 | and contain the values you want to set. 72 | 73 | Example: 74 | 75 | .. code-block:: console 76 | 77 | $ tree --noreport mysite_env/ 78 | mysite_env/ 79 | ├── DJANGO_SETTINGS_MODULE 80 | ├── DJANGO_DEBUG 81 | ├── DJANGO_DATABASE_URL 82 | ├── DJANGO_CACHE_URL 83 | └── PYTHONSTARTUP 84 | 85 | $ cat mysite_env/DJANGO_CACHE_URL 86 | redis://user@host:port/1 87 | 88 | Then, to enable the ``mysite_env`` environment variables, simply use the 89 | ``envdir`` command line tool as a prefix for your program, e.g.: 90 | 91 | .. code-block:: console 92 | 93 | $ envdir mysite_env python manage.py runserver 94 | 95 | See envdir_ documentation for more information, e.g. using envdir_ from 96 | Python instead of from the command line. 97 | 98 | .. _envdir: https://pypi.python.org/pypi/envdir 99 | 100 | Sentry (dynamic setup calls) 101 | ---------------------------- 102 | 103 | For all tools that require an initialization call you should use 104 | :ref:`Setup methods` (unless you want them activated 105 | for all environments). 106 | 107 | Intuitively you might want to add the required setup call like any 108 | other setting: 109 | 110 | .. code-block:: python 111 | 112 | class Prod(Base): 113 | # ... 114 | 115 | sentry_sdk.init("your dsn", integrations=[DjangoIntegration()]) 116 | 117 | But this will activate, in this case, Sentry even when you're running a 118 | Dev configuration. What you should do instead, is put that code in the 119 | ``post_setup`` function. That way Sentry will only ever run when Prod 120 | is the selected configuration: 121 | 122 | .. code-block:: python 123 | 124 | class Prod(Base): 125 | # ... 126 | 127 | @classmethod 128 | def post_setup(cls): 129 | """Sentry initialization""" 130 | super(Prod, cls).post_setup() 131 | sentry_sdk.init( 132 | dsn=os.environ.get("your dsn"), integrations=[DjangoIntegration()] 133 | ) 134 | 135 | 136 | .. _project-templates: 137 | 138 | Project templates 139 | ----------------- 140 | 141 | You can use a special Django project template that is a copy of the one 142 | included in Django 1.5.x and 1.6.x. The following examples assumes you're 143 | using pip_ to install packages. 144 | 145 | Django 1.8.x 146 | ^^^^^^^^^^^^ 147 | 148 | First install Django 1.8.x and django-configurations: 149 | 150 | .. code-block:: console 151 | 152 | $ python -m pip install -r https://raw.github.com/jazzband/django-configurations/templates/1.8.x/requirements.txt 153 | 154 | Or Django 1.8: 155 | 156 | .. code-block:: console 157 | 158 | $ python -m django startproject mysite -v2 --template https://github.com/jazzband/django-configurations/archive/templates/1.8.x.zip 159 | 160 | Now you have a default Django 1.8.x project in the ``mysite`` 161 | directory that uses django-configurations. 162 | 163 | See the repository of the template for more information: 164 | 165 | https://github.com/jazzband/django-configurations/tree/templates/1.8.x 166 | 167 | .. _pip: http://pip-installer.org/ 168 | 169 | Celery 170 | ------ 171 | 172 | < 3.1 173 | ^^^^^ 174 | 175 | Given Celery's way to load Django settings in worker processes you should 176 | probably just add the following to the **beginning** of your settings module: 177 | 178 | .. code-block:: python 179 | 180 | import configurations 181 | configurations.setup() 182 | 183 | That has the same effect as using the ``manage.py``, ``wsgi.py`` or ``asgi.py`` utilities. 184 | This will also call ``django.setup()``. 185 | 186 | >= 3.1 187 | ^^^^^^ 188 | 189 | In Celery 3.1 and later the integration between Django and Celery has been 190 | simplified to use the standard Celery Python API. Django projects using Celery 191 | are now advised to add a ``celery.py`` file that instantiates an explicit 192 | ``Celery`` client app. 193 | 194 | Here's how to integrate django-configurations following the `example from 195 | Celery's documentation`_: 196 | 197 | .. code-block:: python 198 | :emphasize-lines: 9, 11-12 199 | 200 | from __future__ import absolute_import 201 | 202 | import os 203 | 204 | from celery import Celery 205 | from django.conf import settings 206 | 207 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 208 | os.environ.setdefault('DJANGO_CONFIGURATION', 'MySiteConfiguration') 209 | 210 | import configurations 211 | configurations.setup() 212 | 213 | app = Celery('mysite') 214 | app.config_from_object('django.conf:settings') 215 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 216 | 217 | @app.task(bind=True) 218 | def debug_task(self): 219 | print('Request: {0!r}'.format(self.request)) 220 | 221 | .. _`example from Celery's documentation`: http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html 222 | 223 | 224 | iPython notebooks 225 | ----------------- 226 | 227 | .. versionadded:: 0.6 228 | 229 | To use django-configurations with IPython_'s great notebooks, you have to 230 | enable an extension in your IPython configuration. See the IPython 231 | documentation for how to create and `manage your IPython profile`_ correctly. 232 | 233 | Here's a quick how-to in case you don't have a profile yet. Type in your 234 | command line shell: 235 | 236 | .. code-block:: console 237 | 238 | $ ipython profile create 239 | 240 | Then let IPython show you where the configuration file ``ipython_config.py`` 241 | was created: 242 | 243 | .. code-block:: console 244 | 245 | $ ipython locate profile 246 | 247 | That should print a directory path where you can find the 248 | ``ipython_config.py`` configuration file. Now open that file and extend the 249 | ``c.InteractiveShellApp.extensions`` configuration value. It may be commented 250 | out from when IPython created the file or it may not exist in the file at all. 251 | In either case make sure it's not a Python comment anymore and reads like this: 252 | 253 | .. code-block:: python 254 | 255 | # A list of dotted module names of IPython extensions to load. 256 | c.InteractiveShellApp.extensions = [ 257 | # .. your other extensions if available 258 | 'configurations', 259 | ] 260 | 261 | That will tell IPython to load django-configurations correctly on startup. 262 | It also works with django-extensions's shell_plus_ management command. 263 | 264 | .. _IPython: http://ipython.org/ 265 | .. _`manage your IPython profile`: http://ipython.org/ipython-doc/dev/config/overview.html#configuration-file-location 266 | .. _shell_plus: https://django-extensions.readthedocs.io/en/latest/shell_plus.html 267 | 268 | 269 | FastCGI 270 | ------- 271 | 272 | In case you use FastCGI for deploying Django (you really shouldn't) and aren't 273 | allowed to use Django's runfcgi_ management command (that would automatically 274 | handle the setup for your if you've followed the quickstart guide above), make 275 | sure to use something like the following script: 276 | 277 | .. code-block:: python 278 | 279 | #!/usr/bin/env python 280 | 281 | import os 282 | 283 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 284 | os.environ.setdefault('DJANGO_CONFIGURATION', 'MySiteConfiguration') 285 | 286 | from configurations.fastcgi import runfastcgi 287 | 288 | runfastcgi(method='threaded', daemonize='true') 289 | 290 | As you can see django-configurations provides a helper module 291 | ``configurations.fastcgi`` that handles the setup of your configurations. 292 | 293 | .. _runfcgi: https://docs.djangoproject.com/en/1.5/howto/deployment/fastcgi/ 294 | 295 | 296 | Sphinx 297 | ------ 298 | 299 | In case you would like to user the amazing `autodoc` feature of the 300 | documentation tool `Sphinx `_, you need add 301 | django-configurations to your ``extensions`` config variable and set 302 | the environment variable accordingly: 303 | 304 | .. code-block:: python 305 | :emphasize-lines: 2-3, 12 306 | 307 | # My custom Django environment variables 308 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 309 | os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev') 310 | 311 | # Add any Sphinx extension module names here, as strings. They can be extensions 312 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 313 | extensions = [ 314 | 'sphinx.ext.autodoc', 315 | 'sphinx.ext.intersphinx', 316 | 'sphinx.ext.viewcode', 317 | # ... 318 | 'configurations.sphinx', 319 | ] 320 | 321 | # ... 322 | 323 | .. versionchanged:: 2.0 324 | 325 | Please note that the sphinx callable has been moved from ``configurations`` to 326 | ``configurations.sphinx``. 327 | 328 | 329 | Channels 330 | -------- 331 | 332 | If you want to deploy a project that uses the Django channels with 333 | `Daphne `_ as the 334 | `interface server `_ 335 | you have to use a asgi.py script similar to the following: 336 | 337 | .. code-block:: python 338 | 339 | import os 340 | from configurations import importer 341 | from channels.asgi import get_channel_layer 342 | 343 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings") 344 | os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev') 345 | 346 | importer.install() 347 | 348 | channel_layer = get_channel_layer() 349 | 350 | That will properly load your django-configurations powered settings. 351 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Project templates 4 | ^^^^^^^^^^^^^^^^^ 5 | 6 | Don't miss the Django :ref:`project templates pre-configured with 7 | django-configurations` to simplify getting started 8 | with new Django projects. 9 | 10 | Wait, what? 11 | ----------- 12 | 13 | django-configurations helps you organize the configuration of your Django 14 | project by providing the glue code to bridge between Django's module based 15 | settings system and programming patterns like mixins_, facades_, factories_ 16 | and adapters_ that are useful for non-trivial configuration scenarios. 17 | 18 | It allows you to use the native abilities of Python inheritance without the 19 | side effects of module level namespaces that often lead to the unfortunate 20 | use of the ``from foo import *`` anti-pattern. 21 | 22 | .. _mixins: http://en.wikipedia.org/wiki/Mixin 23 | .. _facades: http://en.wikipedia.org/wiki/Facade_pattern 24 | .. _factories: http://en.wikipedia.org/wiki/Factory_method_pattern 25 | .. _adapters: http://en.wikipedia.org/wiki/Adapter_pattern 26 | 27 | Okay, how does it work? 28 | ----------------------- 29 | 30 | Any subclass of the ``configurations.Configuration`` class will automatically 31 | use the values of its class and instance attributes (including properties 32 | and methods) to set module level variables of the same module -- that's 33 | how Django will interface to the django-configurations based settings during 34 | startup and also the reason why it requires you to use its own startup 35 | functions. 36 | 37 | That means when Django starts up django-configurations will have a look at 38 | the ``DJANGO_CONFIGURATION`` environment variable to figure out which class 39 | in the settings module (as defined by the ``DJANGO_SETTINGS_MODULE`` 40 | environment variable) should be used for the process. It then instantiates 41 | the class defined with ``DJANGO_CONFIGURATION`` and copies the uppercase 42 | attributes to the module level variables. 43 | 44 | .. versionadded:: 0.2 45 | 46 | Alternatively you can use the ``--configuration`` command line option that 47 | django-configurations adds to all Django management commands. Behind the 48 | scenes it will simply set the ``DJANGO_CONFIGURATION`` environement variable 49 | so this is purely optional and just there to compliment the default 50 | ``--settings`` option that Django adds if you prefer that instead of setting 51 | environment variables. 52 | 53 | But isn't that magic? 54 | --------------------- 55 | 56 | Yes, it looks like magic, but it's also maintainable and non-intrusive. 57 | No monkey patching is needed to teach Django how to load settings via 58 | django-configurations because it uses Python import hooks (`PEP 302`_) 59 | behind the scenes. 60 | 61 | .. _`PEP 302`: http://www.python.org/dev/peps/pep-0302/ 62 | 63 | Further documentation 64 | --------------------- 65 | 66 | .. toctree:: 67 | :maxdepth: 3 68 | 69 | patterns 70 | values 71 | cookbook 72 | changes 73 | 74 | Alternatives 75 | ------------ 76 | 77 | Many thanks to those project that have previously solved these problems: 78 | 79 | - The Pinax_ project for spearheading the efforts to extend the Django 80 | project metaphor with reusable project templates and a flexible 81 | configuration environment. 82 | 83 | - `django-classbasedsettings`_ by Matthew Tretter for being the immediate 84 | inspiration for django-configurations. 85 | 86 | .. _Pinax: http://pinaxproject.com 87 | .. _`django-classbasedsettings`: https://github.com/matthewwithanm/django-classbasedsettings 88 | 89 | 90 | Bugs and feature requests 91 | ------------------------- 92 | 93 | As always your mileage may vary, so please don't hesitate to send feature 94 | requests and bug reports: 95 | 96 | - https://github.com/jazzband/django-configurations/issues 97 | 98 | Thanks! -------------------------------------------------------------------------------- /docs/patterns.rst: -------------------------------------------------------------------------------- 1 | Usage patterns 2 | ============== 3 | 4 | There are various configuration patterns that can be implemented with 5 | django-configurations. The most common pattern is to have a base class 6 | and various subclasses based on the environment they are supposed to be 7 | used in, e.g. in production, staging and development. 8 | 9 | Server specific settings 10 | ------------------------ 11 | 12 | For example, imagine you have a base setting class in your **settings.py** 13 | file: 14 | 15 | .. code-block:: python 16 | 17 | from configurations import Configuration 18 | 19 | class Base(Configuration): 20 | TIME_ZONE = 'Europe/Berlin' 21 | 22 | class Dev(Base): 23 | DEBUG = True 24 | 25 | class Prod(Base): 26 | TIME_ZONE = 'America/New_York' 27 | 28 | You can now set the ``DJANGO_CONFIGURATION`` environment variable to 29 | one of the class names you've defined, e.g. on your production server 30 | it should be ``Prod``. In Bash that would be: 31 | 32 | .. code-block:: console 33 | 34 | $ export DJANGO_SETTINGS_MODULE=mysite.settings 35 | $ export DJANGO_CONFIGURATION=Prod 36 | $ python -m manage runserver 37 | 38 | Alternatively you can use the ``--configuration`` option when using Django 39 | management commands along the lines of Django's default ``--settings`` 40 | command line option, e.g. 41 | 42 | .. code-block:: console 43 | 44 | $ python -m manage runserver --settings=mysite.settings --configuration=Prod 45 | 46 | Property settings 47 | ----------------- 48 | 49 | Use a ``property`` to allow for computed settings. This pattern can 50 | also be used to postpone / lazy evaluate a value. E.g., useful when 51 | nesting a Value in a dictionary and a string is required: 52 | 53 | .. code-block:: python 54 | 55 | class Prod(Configuration): 56 | SOME_VALUE = values.Value(None, environ_prefix=None) 57 | 58 | @property 59 | def SOME_CONFIG(self): 60 | return { 61 | 'some_key': self.SOME_VALUE, 62 | } 63 | 64 | Global settings defaults 65 | ------------------------ 66 | 67 | Every ``configurations.Configuration`` subclass will automatically 68 | contain Django's global settings as class attributes, so you can refer 69 | to them when setting other values, e.g. 70 | 71 | .. code-block:: python 72 | 73 | from configurations import Configuration 74 | 75 | class Prod(Configuration): 76 | TEMPLATE_CONTEXT_PROCESSORS = Configuration.TEMPLATE_CONTEXT_PROCESSORS + ( 77 | 'django.core.context_processors.request', 78 | ) 79 | 80 | @property 81 | def LANGUAGES(self): 82 | return list(Configuration.LANGUAGES) + [('tlh', 'Klingon')] 83 | 84 | Configuration mixins 85 | -------------------- 86 | 87 | You might want to apply some configuration values for each and every 88 | project you're working on without having to repeat yourself. Just define 89 | a few mixin you re-use multiple times: 90 | 91 | .. code-block:: python 92 | 93 | class FullPageCaching: 94 | USE_ETAGS = True 95 | 96 | Then import that mixin class in your site settings module and use it with 97 | a ``Configuration`` class: 98 | 99 | .. code-block:: python 100 | 101 | from configurations import Configuration 102 | 103 | class Prod(FullPageCaching, Configuration): 104 | DEBUG = False 105 | # ... 106 | 107 | Pristine methods 108 | ---------------- 109 | 110 | .. versionadded:: 0.3 111 | 112 | In case one of your settings itself need to be a callable, you need to 113 | tell that django-configurations by using the ``pristinemethod`` 114 | decorator, e.g. 115 | 116 | .. code-block:: python 117 | 118 | from configurations import Configuration, pristinemethod 119 | 120 | class Prod(Configuration): 121 | 122 | @pristinemethod 123 | def ACCESS_FUNCTION(user): 124 | return user.is_staff 125 | 126 | Lambdas work, too: 127 | 128 | .. code-block:: python 129 | 130 | from configurations import Configuration, pristinemethod 131 | 132 | class Prod(Configuration): 133 | ACCESS_FUNCTION = pristinemethod(lambda user: user.is_staff) 134 | 135 | 136 | .. _setup-methods: 137 | 138 | Setup methods 139 | ------------- 140 | 141 | .. versionadded:: 0.3 142 | 143 | If there is something required to be set up before, during or after the 144 | settings loading happens, please override the ``pre_setup``, ``setup`` or 145 | ``post_setup`` class methods like so (don't forget to apply the Python 146 | ``@classmethod`` decorator): 147 | 148 | .. code-block:: python 149 | 150 | import logging 151 | from configurations import Configuration 152 | 153 | class Prod(Configuration): 154 | # ... 155 | 156 | @classmethod 157 | def pre_setup(cls): 158 | super(Prod, cls).pre_setup() 159 | if something.completely.different(): 160 | cls.DEBUG = True 161 | 162 | @classmethod 163 | def setup(cls): 164 | super(Prod, cls).setup() 165 | logging.info('production settings loaded: %s', cls) 166 | 167 | @classmethod 168 | def post_setup(cls): 169 | super(Prod, cls).post_setup() 170 | logging.debug("done setting up! \o/") 171 | 172 | As you can see above the ``pre_setup`` method can also be used to 173 | programmatically change a class attribute of the settings class and it 174 | will be taken into account when doing the rest of the settings setup. 175 | Of course that won't work for ``post_setup`` since that's when the 176 | settings setup is already done. 177 | 178 | In fact you can easily do something unrelated to settings, like 179 | connecting to a database: 180 | 181 | .. code-block:: python 182 | 183 | from configurations import Configuration 184 | 185 | class Prod(Configuration): 186 | # ... 187 | 188 | @classmethod 189 | def post_setup(cls): 190 | import mango 191 | mango.connect('enterprise') 192 | 193 | .. warning:: 194 | 195 | You could do the same by overriding the ``__init__`` method of your 196 | settings class but this may cause hard to debug errors because 197 | at the time the ``__init__`` method is called (during Django 198 | startup) the Django setting system isn't fully loaded yet. 199 | 200 | So anything you do in ``__init__`` that may require 201 | ``django.conf.settings`` or Django models there is a good chance it 202 | won't work. Use the ``post_setup`` method for that instead. 203 | 204 | .. versionchanged:: 0.4 205 | 206 | A new ``setup`` method was added to be able to handle the new 207 | :class:`~configurations.values.Value` classes and allow an 208 | in-between modification of the configuration values. 209 | 210 | Standalone scripts 211 | ------------------ 212 | 213 | If you want to run scripts outside of your project you need to add 214 | these lines on top of your file: 215 | 216 | .. code-block:: python 217 | 218 | import configurations 219 | configurations.setup() 220 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx>4 2 | furo 3 | docutils 4 | -------------------------------------------------------------------------------- /docs/values.rst: -------------------------------------------------------------------------------- 1 | Values 2 | ====== 3 | 4 | .. module:: configurations.values 5 | :synopsis: Optional value classes for high-level validation and behavior. 6 | 7 | .. versionadded:: 0.4 8 | 9 | django-configurations allows you to optionally reduce the amount of validation 10 | and setup code in your **settings.py** by using ``Value`` classes. They have 11 | the ability to handle values from the process environment of your software 12 | (:data:`os.environ`) and work well in projects that follow the 13 | `Twelve-Factor methodology`_. 14 | 15 | .. note:: 16 | 17 | These classes are required to be used as attributes of ``Configuration`` 18 | classes. See the :doc:`main documentation` for more information. 19 | 20 | Overview 21 | -------- 22 | 23 | Here is an example (from a **settings.py** file with a ``Configuration`` 24 | subclass): 25 | 26 | .. code-block:: python 27 | :emphasize-lines: 4 28 | 29 | from configurations import Configuration, values 30 | 31 | class Dev(Configuration): 32 | DEBUG = values.BooleanValue(True) 33 | 34 | As you can see all you have to do is to wrap your settings value in a call 35 | to one of the included values classes. When Django's process starts up 36 | it will automatically make sure the passed-in value validates correctly -- 37 | in the above case checks if the value is really a boolean. 38 | 39 | You can safely use other :class:`~Value` instances as the default setting 40 | value: 41 | 42 | .. code-block:: python 43 | :emphasize-lines: 5 44 | 45 | from configurations import Configuration, values 46 | 47 | class Dev(Configuration): 48 | DEBUG = values.BooleanValue(True) 49 | DEBUG_PROPAGATE_EXCEPTIONS = values.BooleanValue(DEBUG) 50 | 51 | See the list of :ref:`built-in value classes` for more information. 52 | 53 | Environment variables 54 | --------------------- 55 | 56 | To separate the site configuration from your application code you should use 57 | environment variables for configuration. Unfortunately environment variables 58 | are string based so they are not easily mapped to the Python based settings 59 | system Django uses. 60 | 61 | Luckily django-configurations' :class:`~Value` subclasses have the ability 62 | to handle environment variables for the common use cases. 63 | 64 | Default behavior 65 | ^^^^^^^^^^^^^^^^ 66 | 67 | For example, imagine you want to override the ``ROOT_URLCONF`` setting on your 68 | staging server to be able to debug a problem with your in-development code. 69 | You're using a web server that passes the environment variables from 70 | the shell it was started from into your Django WSGI process. 71 | 72 | Use the boolean ``environ`` option of the :class:`~Value` class (``True`` by 73 | default) to tell django-configurations to look for an environment variable with 74 | the same name as the specific :class:`~Value` variable, only uppercased and 75 | prefixed with ``DJANGO_``. E.g.: 76 | 77 | .. code-block:: python 78 | :emphasize-lines: 5 79 | 80 | from configurations import Configuration, values 81 | 82 | class Stage(Configuration): 83 | # .. 84 | ROOT_URLCONF = values.Value('mysite.urls') 85 | 86 | django-configurations will try to read the ``DJANGO_ROOT_URLCONF`` environment 87 | variable when deciding which value the ``ROOT_URLCONF`` setting should have. 88 | When you run the web server simply specify that environment variable 89 | (e.g. in your init script): 90 | 91 | .. code-block:: console 92 | 93 | $ DJANGO_ROOT_URLCONF=mysite.debugging_urls gunicorn mysite.wsgi:application 94 | 95 | If the environment variable can't be found it'll use the default 96 | ``'mysite.urls'``. 97 | 98 | Disabling environment variables 99 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 100 | 101 | To disable environment variables, specify the ``environ`` parameter of the 102 | :class:`~Value` class. For example this would disable it for the ``TIME_ZONE`` 103 | setting value:: 104 | 105 | from configurations import Configuration, values 106 | 107 | class Dev(Configuration): 108 | TIME_ZONE = values.Value('UTC', environ=False) 109 | 110 | Custom environment variable names 111 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 112 | 113 | To support legacy systems, integrate with other parts of your software stack or 114 | simply better match your taste in naming public configuration variables, 115 | django-configurations allows you to use the ``environ_name`` parameter of the 116 | :class:`~Value` class to change the base name of the environment variable it 117 | looks for. For example this would enforce the name ``DJANGO_MYSITE_TZ`` 118 | instead of using the name of the :class:`~Value` instance.:: 119 | 120 | from configurations import Configuration, values 121 | 122 | class Dev(Configuration): 123 | TIME_ZONE = values.Value('UTC', environ_name='MYSITE_TZ') 124 | 125 | Allow final value to be used outside the configuration context 126 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 127 | 128 | You may use the ``environ_name`` parameter to allow a :class:`~Value` to be 129 | directly converted to its final value for use outside of the configuration 130 | context: 131 | 132 | .. code-block:: pycon 133 | 134 | >>> type(values.Value([])) 135 | 136 | >>> type(values.Value([], environ_name="FOOBAR")) 137 | 138 | 139 | This can also be achieved when using ``environ=False`` and providing a 140 | default value. 141 | 142 | Custom environment variable prefixes 143 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 144 | 145 | In case you want to change the default environment variable name prefix 146 | of ``DJANGO`` to something to your likening, use the ``environ_prefix`` 147 | parameter of the :class:`~Value` instance. Here it'll look for the 148 | ``MYSITE_TIME_ZONE`` environment variable (instead of ``DJANGO_TIME_ZONE``):: 149 | 150 | from configurations import Configuration, values 151 | 152 | class Dev(Configuration): 153 | TIME_ZONE = values.Value('UTC', environ_prefix='MYSITE') 154 | 155 | The ``environ_prefix`` parameter can also be ``None`` to completely disable 156 | the prefix. 157 | 158 | ``Value`` class 159 | --------------- 160 | 161 | .. class:: Value(default, [environ=True, environ_name=None, environ_prefix='DJANGO', environ_required=False]) 162 | 163 | The ``Value`` class takes one required and several optional parameters. 164 | 165 | :param default: the default value of the setting 166 | :param environ: toggle for environment use 167 | :param environ_name: capitalized name of environment variable to look for 168 | :param environ_prefix: capitalized prefix to use when looking for environment variable 169 | :param environ_required: whether or not the value is required to be set as an environment variable 170 | :type environ: bool 171 | :type environ_name: str or None 172 | :type environ_prefix: str 173 | :type environ_required: bool 174 | 175 | The ``default`` parameter is effectively the value the setting has 176 | right now in your ``settings.py``. 177 | 178 | .. method:: setup(name) 179 | 180 | :param name: the name of the setting 181 | :return: setting value 182 | 183 | The ``setup`` method is called during startup of the Django process and 184 | implements the ability to check the environment variable. Its purpose is 185 | to return a value django-configurations is supposed to use when loading 186 | the settings. It'll be passed one parameter, the name of the 187 | :class:`~Value` instance as defined in the ``settings.py``. This is used 188 | for building the name of the environment variable. 189 | 190 | .. method:: to_python(value) 191 | 192 | :param value: the value of the setting as found in the process 193 | environment (:data:`os.environ`) 194 | :return: validated and "ready" setting value if found in process 195 | environment 196 | 197 | The ``to_python`` method is used when the ``environ`` parameter of the 198 | :class:`~Value` class is set to ``True`` (the default) and an 199 | environment variable with the appropriate name was found. 200 | 201 | It will be used to handle the string based environment variables and 202 | returns the "ready" value of the setting. 203 | 204 | Some :class:`~Value` subclasses also use it during initialization when the 205 | default value has a string-like format like an environment variable which 206 | needs to be converted into a Python data type. 207 | 208 | .. _built-ins: 209 | 210 | Built-ins 211 | --------- 212 | 213 | Type values 214 | ^^^^^^^^^^^ 215 | 216 | .. class:: BooleanValue 217 | 218 | A :class:`~Value` subclass that checks and returns boolean values. Possible 219 | values for environment variables are: 220 | 221 | - ``True`` values: ``'yes'``, ``'y'``, ``'true'``, ``'1'`` 222 | - ``False`` values: ``'no'``, ``'n'``, ``'false'``, ``'0'``, 223 | ``''`` (empty string) 224 | 225 | :: 226 | 227 | DEBUG = values.BooleanValue(True) 228 | 229 | .. class:: IntegerValue 230 | 231 | A :class:`~Value` subclass that handles integer values. 232 | 233 | :: 234 | 235 | MYSITE_CACHE_TIMEOUT = values.IntegerValue(3600) 236 | 237 | .. class:: PositiveIntegerValue 238 | 239 | A :class:`~Value` subclass that handles positive integer values. 240 | 241 | .. versionadded:: 2.1 242 | 243 | :: 244 | 245 | MYSITE_WORKER_POOL = values.PositiveIntegerValue(8) 246 | 247 | .. class:: FloatValue 248 | 249 | A :class:`~Value` subclass that handles float values. 250 | 251 | :: 252 | 253 | MYSITE_TAX_RATE = values.FloatValue(11.9) 254 | 255 | .. class:: DecimalValue 256 | 257 | A :class:`~Value` subclass that handles Decimal values. 258 | 259 | :: 260 | 261 | MYSITE_CONVERSION_RATE = values.DecimalValue(decimal.Decimal('4.56214')) 262 | 263 | .. class:: SequenceValue 264 | 265 | Common base class for sequence values. 266 | 267 | .. class:: ListValue(default, [separator=',', converter=None]) 268 | 269 | A :class:`~SequenceValue` subclass that handles list values. 270 | 271 | :param separator: the separator to split environment variables with 272 | :param converter: the optional converter callable to apply for each list 273 | item 274 | 275 | Simple example:: 276 | 277 | ALLOWED_HOSTS = ListValue(['mysite.com', 'mysite.biz']) 278 | 279 | Use a custom converter to check for the given variables:: 280 | 281 | def check_monty_python(person): 282 | if not is_completely_different(person): 283 | error = '{0} is not a Monty Python member'.format(person) 284 | raise ValueError(error) 285 | return person 286 | 287 | MONTY_PYTHONS = ListValue(['John Cleese', 'Eric Idle'], 288 | converter=check_monty_python) 289 | 290 | You can override this list with an environment variable like this: 291 | 292 | .. code-block:: console 293 | 294 | $ DJANGO_MONTY_PYTHONS="Terry Jones,Graham Chapman" gunicorn mysite.wsgi:application 295 | 296 | Use a custom separator:: 297 | 298 | EMERGENCY_EMAILS = ListValue(['admin@mysite.net'], separator=';') 299 | 300 | And override it: 301 | 302 | .. code-block:: console 303 | 304 | $ DJANGO_EMERGENCY_EMAILS="admin@mysite.net;manager@mysite.org;support@mysite.com" gunicorn mysite.wsgi:application 305 | 306 | .. class:: TupleValue 307 | 308 | A :class:`~SequenceValue` subclass that handles tuple values. 309 | 310 | :param separator: the separator to split environment variables with 311 | :param converter: the optional converter callable to apply for each tuple 312 | item 313 | 314 | See the :class:`~ListValue` examples above. 315 | 316 | .. class:: SingleNestedSequenceValue 317 | 318 | Common base class for nested sequence values. 319 | 320 | .. class:: SingleNestedTupleValue(default, [seq_separator=';', separator=',', converter=None]) 321 | 322 | A :class:`~SingleNestedSequenceValue` subclass that handles single nested tuple values, 323 | e.g. ``((a, b), (c, d))``. 324 | 325 | :param seq_separator: the separator to split each tuple with 326 | :param separator: the separator to split the inner tuple contents with 327 | :param converter: the optional converter callable to apply for each inner 328 | tuple item 329 | 330 | Useful for ADMINS, MANAGERS, and the like. For example:: 331 | 332 | ADMINS = SingleNestedTupleValue(( 333 | ('John', 'jcleese@site.com'), 334 | ('Eric', 'eidle@site.com'), 335 | )) 336 | 337 | Override using environment variables like this:: 338 | 339 | DJANGO_ADMINS=Terry,tjones@site.com;Graham,gchapman@site.com 340 | 341 | .. class:: SingleNestedListValue(default, [seq_separator=';', separator=',', converter=None]) 342 | 343 | A :class:`~SingleNestedSequenceValue` subclass that handles single nested list values, 344 | e.g. ``[[a, b], [c, d]]``. 345 | 346 | :param seq_separator: the separator to split each list with 347 | :param separator: the separator to split the inner list contents with 348 | :param converter: the optional converter callable to apply for each inner 349 | list item 350 | 351 | See the :class:`~SingleNestedTupleValue` examples above. 352 | 353 | .. class:: SetValue 354 | 355 | A :class:`~Value` subclass that handles set values. 356 | 357 | :param separator: the separator to split environment variables with 358 | :param converter: the optional converter callable to apply for each set 359 | item 360 | 361 | See the :class:`~ListValue` examples above. 362 | 363 | .. class:: DictValue 364 | 365 | A :class:`~Value` subclass that handles dicts. 366 | 367 | :: 368 | 369 | DEPARTMENTS = values.DictValue({ 370 | 'it': ['Mike', 'Joe'], 371 | }) 372 | 373 | Override using environment variables like this:: 374 | 375 | DJANGO_DEPARTMENTS={'it':['Mike','Joe'],'hr':['Emma','Olivia']} 376 | 377 | Validator values 378 | ^^^^^^^^^^^^^^^^ 379 | 380 | .. class:: EmailValue 381 | 382 | A :class:`~Value` subclass that validates the value using the 383 | :data:`django:django.core.validators.validate_email` validator. 384 | 385 | :: 386 | 387 | SUPPORT_EMAIL = values.EmailValue('support@mysite.com') 388 | 389 | .. class:: URLValue 390 | 391 | A :class:`~Value` subclass that validates the value using the 392 | :class:`django:django.core.validators.URLValidator` validator. 393 | 394 | :: 395 | 396 | SUPPORT_URL = values.URLValue('https://support.mysite.com/') 397 | 398 | .. class:: IPValue 399 | 400 | A :class:`~Value` subclass that validates the value using the 401 | :data:`django:django.core.validators.validate_ipv46_address` validator. 402 | 403 | :: 404 | 405 | LOADBALANCER_IP = values.IPValue('127.0.0.1') 406 | 407 | .. class:: RegexValue(default, regex, [environ=True, environ_name=None, environ_prefix='DJANGO']) 408 | 409 | A :class:`~Value` subclass that validates according a regular expression 410 | and uses the :class:`django:django.core.validators.RegexValidator`. 411 | 412 | :param regex: the regular expression 413 | 414 | :: 415 | 416 | DEFAULT_SKU = values.RegexValue('000-000-00', regex=r'\d{3}-\d{3}-\d{2}') 417 | 418 | .. class:: PathValue(default, [check_exists=True, environ=True, environ_name=None, environ_prefix='DJANGO']) 419 | 420 | A :class:`~Value` subclass that normalizes the given path using 421 | :func:`os.path.expanduser` and checks if it exists on the file system. 422 | 423 | Takes an optional ``check_exists`` parameter to disable the check with 424 | :func:`os.path.exists`. 425 | 426 | :param check_exists: toggle the file system check 427 | 428 | :: 429 | 430 | BASE_DIR = values.PathValue('/opt/mysite/') 431 | STATIC_ROOT = values.PathValue('/var/www/static', checks_exists=False) 432 | 433 | URL-based values 434 | ^^^^^^^^^^^^^^^^ 435 | 436 | .. note:: 437 | 438 | The following URL-based :class:`~Value` subclasses are inspired by the 439 | `Twelve-Factor methodology`_ and use environment variable names that are 440 | already established by that methodology, e.g. ``'DATABASE_URL'``. 441 | 442 | Each of these classes require external libraries to be installed, e.g. the 443 | :class:`~DatabaseURLValue` class depends on the package ``dj-database-url``. 444 | See the specific class documentation below for which package is needed. 445 | 446 | .. class:: DatabaseURLValue(default, [alias='default', environ=True, environ_name='DATABASE_URL', environ_prefix=None]) 447 | 448 | A :class:`~Value` subclass that uses the `dj-database-url`_ app to 449 | convert a database configuration value stored in the ``DATABASE_URL`` 450 | environment variable into an appropriate setting value. It's inspired by 451 | the `Twelve-Factor methodology`_. 452 | 453 | By default this :class:`~Value` subclass looks for the ``DATABASE_URL`` 454 | environment variable. 455 | 456 | Takes an optional ``alias`` parameter to define which database alias to 457 | use for the ``DATABASES`` setting. 458 | 459 | :param alias: which database alias to use 460 | 461 | The other parameters have the following default values: 462 | 463 | :param environ: ``True`` 464 | :param environ_name: ``DATABASE_URL`` 465 | :param environ_prefix: ``None`` 466 | 467 | :: 468 | 469 | DATABASES = values.DatabaseURLValue('postgres://myuser@localhost/mydb') 470 | 471 | .. _`dj-database-url`: https://pypi.python.org/pypi/dj-database-url/ 472 | 473 | .. class:: CacheURLValue(default, [alias='default', environ=True, environ_name='CACHE_URL', environ_prefix=None]) 474 | 475 | A :class:`~Value` subclass that uses the `django-cache-url`_ app to 476 | convert a cache configuration value stored in the ``CACHE_URL`` 477 | environment variable into an appropriate setting value. It's inspired by 478 | the `Twelve-Factor methodology`_. 479 | 480 | By default this :class:`~Value` subclass looks for the ``CACHE_URL`` 481 | environment variable. 482 | 483 | Takes an optional ``alias`` parameter to define which database alias to 484 | use for the ``CACHES`` setting. 485 | 486 | :param alias: which cache alias to use 487 | 488 | The other parameters have the following default values: 489 | 490 | :param environ: ``True`` 491 | :param environ_name: ``CACHE_URL`` 492 | :param environ_prefix: ``None`` 493 | 494 | :: 495 | 496 | CACHES = values.CacheURLValue('memcached://127.0.0.1:11211/') 497 | 498 | .. _`django-cache-url`: https://pypi.python.org/pypi/django-cache-url/ 499 | 500 | .. class:: EmailURLValue(default, [environ=True, environ_name='EMAIL_URL', environ_prefix=None]) 501 | 502 | A :class:`~Value` subclass that uses the `dj-email-url`_ app to 503 | convert an email configuration value stored in the ``EMAIL_URL`` 504 | environment variable into the appropriate settings. It's inspired by 505 | the `Twelve-Factor methodology`_. 506 | 507 | By default this :class:`~Value` subclass looks for the ``EMAIL_URL`` 508 | environment variable. 509 | 510 | .. note:: 511 | 512 | This is a special value since email settings are divided into many 513 | different settings variables. `dj-email-url`_ supports all options 514 | though and simply returns a nested dictionary of settings instead of 515 | just one setting. 516 | 517 | The parameters have the following default values: 518 | 519 | :param environ: ``True`` 520 | :param environ_name: ``EMAIL_URL`` 521 | :param environ_prefix: ``None`` 522 | 523 | :: 524 | 525 | EMAIL = values.EmailURLValue('console://') 526 | 527 | .. _`dj-email-url`: https://pypi.python.org/pypi/dj-email-url/ 528 | 529 | .. class:: SearchURLValue(default, [environ=True, environ_name='SEARCH_URL', environ_prefix=None]) 530 | 531 | .. versionadded:: 0.8 532 | 533 | A :class:`~Value` subclass that uses the `dj-search-url`_ app to 534 | convert a search configuration value stored in the ``SEARCH_URL`` 535 | environment variable into the appropriate settings for use with Haystack_. 536 | It's inspired by the `Twelve-Factor methodology`_. 537 | 538 | By default this :class:`~Value` subclass looks for the ``SEARCH_URL`` 539 | environment variable. 540 | 541 | Takes an optional ``alias`` parameter to define which search backend alias 542 | to use for the ``HAYSTACK_CONNECTIONS`` setting. 543 | 544 | :param alias: which cache alias to use 545 | 546 | The other parameters have the following default values: 547 | 548 | :param environ: ``True`` 549 | :param environ_name: ``SEARCH_URL`` 550 | :param environ_prefix: ``None`` 551 | 552 | :: 553 | 554 | HAYSTACK_CONNECTIONS = values.SearchURLValue('elasticsearch://127.0.0.1:9200/my-index') 555 | 556 | .. _`dj-search-url`: https://pypi.python.org/pypi/dj-search-url/ 557 | .. _Haystack: http://haystacksearch.org/ 558 | 559 | Other values 560 | ^^^^^^^^^^^^ 561 | 562 | .. class:: BackendsValue 563 | 564 | A :class:`~ListValue` subclass that validates the given list of dotted 565 | import paths by trying to import them. In other words, this checks if 566 | the backends exist. 567 | 568 | :: 569 | 570 | MIDDLEWARE = values.BackendsValue([ 571 | 'django.middleware.common.CommonMiddleware', 572 | 'django.contrib.sessions.middleware.SessionMiddleware', 573 | 'django.middleware.csrf.CsrfViewMiddleware', 574 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 575 | 'django.contrib.messages.middleware.MessageMiddleware', 576 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 577 | ]) 578 | 579 | .. class:: SecretValue 580 | 581 | A :class:`~Value` subclass that doesn't allow setting a default value 582 | during instantiation and force-enables the use of an environment variable 583 | to reduce the risk of accidentally storing secret values in the settings 584 | file. This usually resolves to ``DJANGO_SECRET_KEY`` unless you have 585 | customized the environment variable names. 586 | 587 | :raises: ``ValueError`` when given a default value 588 | 589 | .. versionchanged:: 1.0 590 | 591 | This value class has the ``environ_required`` parameter turned to 592 | ``True``. 593 | 594 | :: 595 | 596 | SECRET_KEY = values.SecretValue() 597 | 598 | Value mixins 599 | ^^^^^^^^^^^^ 600 | 601 | .. class:: CastingMixin 602 | 603 | A mixin to be used with one of the :class:`~Value` subclasses that 604 | requires a ``caster`` class attribute of one of the following types: 605 | 606 | - dotted import path, e.g. ``'mysite.utils.custom_caster'`` 607 | - a callable, e.g. :class:`int` 608 | 609 | Example:: 610 | 611 | class TemparatureValue(CastingMixin, Value): 612 | caster = 'mysite.temperature.fahrenheit_to_celcius' 613 | 614 | Optionally it can take a ``message`` class attribute as the error 615 | message to be shown if the casting fails. Additionally an ``exception`` 616 | parameter can be set to a single or a tuple of exception classes that 617 | are required to be handled during the casting. 618 | 619 | .. class:: ValidationMixin 620 | 621 | A mixin to be used with one of the :class:`~Value` subclasses that 622 | requires a ``validator`` class attribute of one of the following types: 623 | The validator should raise Django's 624 | :exc:`~django.core.exceptions.ValidationError` to indicate a failed 625 | validation attempt. 626 | 627 | - dotted import path, e.g. ``'mysite.validators.custom_validator'`` 628 | - a callable, e.g. :class:`bool` 629 | 630 | Example:: 631 | 632 | class TemparatureValue(ValidationMixin, Value): 633 | validator = 'mysite.temperature.is_valid_temparature' 634 | 635 | Optionally it can take a ``message`` class attribute as the error 636 | message to be shown if the validation fails. 637 | 638 | .. class:: MultipleMixin 639 | 640 | A mixin to be used with one of the :class:`~Value` subclasses that 641 | enables the return value of the :func:`~Value.to_python` to be 642 | interpreted as a dictionary of settings values to be set at once, 643 | instead of using the return value to just set one setting. 644 | 645 | A good example for this mixin is the :class:`~EmailURLValue` value 646 | which requires setting many ``EMAIL_*`` settings. 647 | 648 | .. _`Twelve-Factor methodology`: http://www.12factor.net/ 649 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [coverage:run] 2 | source = . 3 | branch = 1 4 | parallel = 1 5 | [coverage:report] 6 | include = configurations/*,tests/* 7 | 8 | [flake8] 9 | exclude = .tox,docs/*,.eggs 10 | ignore = E501,E127,E128,E124,W503 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import codecs 3 | from setuptools import setup 4 | 5 | 6 | def read(*parts): 7 | filename = os.path.join(os.path.dirname(__file__), *parts) 8 | with codecs.open(filename, encoding='utf-8') as fp: 9 | return fp.read() 10 | 11 | 12 | setup( 13 | name="django-configurations", 14 | use_scm_version={"version_scheme": "post-release", "local_scheme": "dirty-tag"}, 15 | setup_requires=["setuptools_scm"], 16 | url='https://django-configurations.readthedocs.io/', 17 | project_urls={ 18 | 'Source': 'https://github.com/jazzband/django-configurations', 19 | }, 20 | license='BSD', 21 | description="A helper for organizing Django settings.", 22 | long_description=read('README.rst'), 23 | long_description_content_type='text/x-rst', 24 | author='Jannis Leidel', 25 | author_email='jannis@leidel.info', 26 | packages=['configurations'], 27 | entry_points={ 28 | 'console_scripts': [ 29 | 'django-cadmin = configurations.management:execute_from_command_line', 30 | ], 31 | }, 32 | install_requires=[ 33 | 'django>=3.2', 34 | ], 35 | python_requires='>=3.9, <4.0', 36 | extras_require={ 37 | 'cache': ['django-cache-url'], 38 | 'database': ['dj-database-url'], 39 | 'email': ['dj-email-url'], 40 | 'search': ['dj-search-url'], 41 | 'testing': [ 42 | 'django-cache-url>=1.0.0', 43 | 'dj-database-url', 44 | 'dj-email-url', 45 | 'dj-search-url', 46 | ], 47 | }, 48 | classifiers=[ 49 | 'Development Status :: 5 - Production/Stable', 50 | 'Framework :: Django', 51 | 'Framework :: Django :: 3.2', 52 | 'Framework :: Django :: 4.1', 53 | 'Framework :: Django :: 4.2', 54 | 'Framework :: Django :: 5.0', 55 | 'Framework :: Django :: 5.1', 56 | 'Intended Audience :: Developers', 57 | 'License :: OSI Approved :: BSD License', 58 | 'Operating System :: OS Independent', 59 | 'Programming Language :: Python', 60 | 'Programming Language :: Python :: 3', 61 | 'Programming Language :: Python :: 3 :: Only', 62 | 'Programming Language :: Python :: 3.9', 63 | 'Programming Language :: Python :: 3.10', 64 | 'Programming Language :: Python :: 3.11', 65 | 'Programming Language :: Python :: 3.12', 66 | 'Programming Language :: Python :: 3.13', 67 | 'Programming Language :: Python :: Implementation :: PyPy', 68 | 'Topic :: Utilities', 69 | ], 70 | zip_safe=False, 71 | ) 72 | -------------------------------------------------------------------------------- /test_project/.env: -------------------------------------------------------------------------------- 1 | DJANGO_DOTENV_VALUE='is set' -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') 7 | os.environ.setdefault('DJANGO_CONFIGURATION', 'Debug') 8 | 9 | from configurations.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /test_project/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-configurations/3d0d4216ca01e83c2b34d89a026890288fb82e37/test_project/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/settings.py: -------------------------------------------------------------------------------- 1 | from configurations import Configuration, values 2 | 3 | 4 | class Base(Configuration): 5 | # Django settings for test_project project. 6 | 7 | DEBUG = values.BooleanValue(True, environ=True) 8 | 9 | ADMINS = ( 10 | # ('Your Name', 'your_email@example.com'), 11 | ) 12 | 13 | EMAIL_URL = values.EmailURLValue('console://', environ=True) 14 | 15 | MANAGERS = ADMINS 16 | 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 20 | 'NAME': '', # Or path to database file if using sqlite3. 21 | 'USER': '', # Not used with sqlite3. 22 | 'PASSWORD': '', # Not used with sqlite3. 23 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 24 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 25 | } 26 | } 27 | 28 | # Hosts/domain names that are valid for this site; required if DEBUG is False 29 | # See https://docs.djangoproject.com/en/1.4/ref/settings/#allowed-hosts 30 | ALLOWED_HOSTS = [] 31 | 32 | # Local time zone for this installation. Choices can be found here: 33 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 34 | # although not all choices may be available on all operating systems. 35 | # In a Windows environment this must be set to your system time zone. 36 | TIME_ZONE = 'America/Chicago' 37 | 38 | # Language code for this installation. All choices can be found here: 39 | # http://www.i18nguy.com/unicode/language-identifiers.html 40 | LANGUAGE_CODE = 'en-us' 41 | 42 | SITE_ID = 1 43 | 44 | # If you set this to False, Django will make some optimizations so as not 45 | # to load the internationalization machinery. 46 | USE_I18N = True 47 | 48 | # If you set this to False, Django will not format dates, numbers and 49 | # calendars according to the current locale. 50 | USE_L10N = True 51 | 52 | # If you set this to False, Django will not use timezone-aware datetimes. 53 | USE_TZ = True 54 | 55 | # Absolute filesystem path to the directory that will hold user-uploaded files. 56 | # Example: "/home/media/media.lawrence.com/media/" 57 | MEDIA_ROOT = '' 58 | 59 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 60 | # trailing slash. 61 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 62 | MEDIA_URL = '' 63 | 64 | # Absolute path to the directory static files should be collected to. 65 | # Don't put anything in this directory yourself; store your static files 66 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 67 | # Example: "/home/media/media.lawrence.com/static/" 68 | STATIC_ROOT = '' 69 | 70 | # URL prefix for static files. 71 | # Example: "http://media.lawrence.com/static/" 72 | STATIC_URL = '/static/' 73 | 74 | # Additional locations of static files 75 | STATICFILES_DIRS = ( 76 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 77 | # Always use forward slashes, even on Windows. 78 | # Don't forget to use absolute paths, not relative paths. 79 | ) 80 | 81 | # List of finder classes that know how to find static files in 82 | # various locations. 83 | STATICFILES_FINDERS = ( 84 | 'django.contrib.staticfiles.finders.FileSystemFinder', 85 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 86 | ) 87 | 88 | # Make this unique, and don't share it with anybody. 89 | SECRET_KEY = '-9i$j8kcp48(y-v0hiwgycp5jb*_)sy4(swd@#m(j1m*4vfn4w' 90 | 91 | # List of callables that know how to import templates from various sources. 92 | TEMPLATE_LOADERS = ( 93 | 'django.template.loaders.filesystem.Loader', 94 | 'django.template.loaders.app_directories.Loader', 95 | ) 96 | 97 | MIDDLEWARE = ( 98 | 'django.middleware.common.CommonMiddleware', 99 | 'django.contrib.sessions.middleware.SessionMiddleware', 100 | 'django.middleware.csrf.CsrfViewMiddleware', 101 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 102 | 'django.contrib.messages.middleware.MessageMiddleware', 103 | # Uncomment the next line for simple clickjacking protection: 104 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 105 | ) 106 | 107 | ROOT_URLCONF = 'test_project.urls' 108 | 109 | # Python dotted path to the WSGI application used by Django's runserver. 110 | WSGI_APPLICATION = 'test_project.wsgi.application' 111 | 112 | TEMPLATE_DIRS = ( 113 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 114 | # Always use forward slashes, even on Windows. 115 | # Don't forget to use absolute paths, not relative paths. 116 | ) 117 | 118 | INSTALLED_APPS = ( 119 | 'django.contrib.auth', 120 | 'django.contrib.contenttypes', 121 | 'django.contrib.sessions', 122 | 'django.contrib.sites', 123 | 'django.contrib.messages', 124 | 'django.contrib.staticfiles', 125 | # Uncomment the next line to enable the admin: 126 | # 'django.contrib.admin', 127 | # Uncomment the next line to enable admin documentation: 128 | # 'django.contrib.admindocs', 129 | 'configurations', 130 | ) 131 | 132 | # A sample logging configuration. The only tangible logging 133 | # performed by this configuration is to send an email to 134 | # the site admins on every HTTP 500 error when DEBUG=False. 135 | # See http://docs.djangoproject.com/en/dev/topics/logging for 136 | # more details on how to customize your logging configuration. 137 | LOGGING = { 138 | 'version': 1, 139 | 'disable_existing_loggers': False, 140 | 'filters': { 141 | 'require_debug_false': { 142 | '()': 'django.utils.log.RequireDebugFalse' 143 | } 144 | }, 145 | 'handlers': { 146 | 'mail_admins': { 147 | 'level': 'ERROR', 148 | 'filters': ['require_debug_false'], 149 | 'class': 'django.utils.log.AdminEmailHandler' 150 | } 151 | }, 152 | 'loggers': { 153 | 'django.request': { 154 | 'handlers': ['mail_admins'], 155 | 'level': 'ERROR', 156 | 'propagate': True, 157 | }, 158 | } 159 | } 160 | 161 | 162 | class Debug(Base): 163 | YEAH = True 164 | 165 | 166 | class Other(Base): 167 | YEAH = False 168 | -------------------------------------------------------------------------------- /test_project/test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Examples: 9 | # url(r'^$', 'test_project.views.home', name='home'), 10 | # url(r'^test_project/', include('test_project.foo.urls')), 11 | 12 | # Uncomment the admin/doc line below to enable admin documentation: 13 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | # url(r'^admin/', include(admin.site.urls)), 17 | ) 18 | -------------------------------------------------------------------------------- /test_project/test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_project project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application # noqa 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-configurations/3d0d4216ca01e83c2b34d89a026890288fb82e37/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-configurations/3d0d4216ca01e83c2b34d89a026890288fb82e37/tests/settings/__init__.py -------------------------------------------------------------------------------- /tests/settings/base.py: -------------------------------------------------------------------------------- 1 | from configurations import Configuration 2 | 3 | 4 | def test_callback(request): 5 | return {} 6 | 7 | 8 | class Base(Configuration): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/settings/dot_env.py: -------------------------------------------------------------------------------- 1 | from configurations import Configuration, values 2 | 3 | 4 | class DotEnvConfiguration(Configuration): 5 | 6 | DOTENV = 'test_project/.env' 7 | 8 | DOTENV_VALUE = values.Value() 9 | 10 | def DOTENV_VALUE_METHOD(self): 11 | return values.Value(environ_name="DOTENV_VALUE") 12 | -------------------------------------------------------------------------------- /tests/settings/error.py: -------------------------------------------------------------------------------- 1 | from configurations import Configuration 2 | 3 | 4 | class ErrorConfiguration(Configuration): 5 | 6 | @classmethod 7 | def pre_setup(cls): 8 | raise ValueError("Error in pre_setup") 9 | -------------------------------------------------------------------------------- /tests/settings/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | 4 | from configurations import Configuration, pristinemethod 5 | 6 | 7 | class Test(Configuration): 8 | BASE_DIR = os.path.abspath( 9 | os.path.join(os.path.dirname( 10 | os.path.abspath(__file__)), os.pardir)) 11 | 12 | DEBUG = True 13 | 14 | SITE_ID = 1 15 | 16 | SECRET_KEY = str(uuid.uuid4()) 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.sqlite3', 21 | 'NAME': os.path.join(os.path.dirname(__file__), 'test.db'), 22 | } 23 | } 24 | 25 | INSTALLED_APPS = [ 26 | 'django.contrib.sessions', 27 | 'django.contrib.contenttypes', 28 | 'django.contrib.sites', 29 | 'django.contrib.auth', 30 | 'tests', 31 | ] 32 | 33 | ROOT_URLCONF = 'tests.urls' 34 | 35 | @property 36 | def ALLOWED_HOSTS(self): 37 | allowed_hosts = super().ALLOWED_HOSTS[:] 38 | allowed_hosts.append('base') 39 | return allowed_hosts 40 | 41 | ATTRIBUTE_SETTING = True 42 | 43 | _PRIVATE_SETTING = 'ryan' 44 | 45 | @property 46 | def PROPERTY_SETTING(self): 47 | return 1 48 | 49 | def METHOD_SETTING(self): 50 | return 2 51 | 52 | LAMBDA_SETTING = lambda self: 3 # noqa: E731 53 | 54 | PRISTINE_LAMBDA_SETTING = pristinemethod(lambda: 4) 55 | 56 | @pristinemethod 57 | def PRISTINE_FUNCTION_SETTING(): 58 | return 5 59 | 60 | @classmethod 61 | def pre_setup(cls): 62 | cls.PRE_SETUP_TEST_SETTING = 6 63 | 64 | @classmethod 65 | def post_setup(cls): 66 | cls.POST_SETUP_TEST_SETTING = 7 67 | 68 | 69 | class TestWithDefaultSetExplicitely(Test): 70 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 71 | -------------------------------------------------------------------------------- /tests/settings/mixin_inheritance.py: -------------------------------------------------------------------------------- 1 | from configurations import Configuration 2 | 3 | 4 | class Mixin1: 5 | @property 6 | def ALLOWED_HOSTS(self): 7 | allowed_hosts = super().ALLOWED_HOSTS[:] 8 | allowed_hosts.append('test1') 9 | return allowed_hosts 10 | 11 | 12 | class Mixin2: 13 | 14 | @property 15 | def ALLOWED_HOSTS(self): 16 | allowed_hosts = super().ALLOWED_HOSTS[:] 17 | allowed_hosts.append('test2') 18 | return allowed_hosts 19 | 20 | 21 | class Inheritance(Mixin2, Mixin1, Configuration): 22 | 23 | def ALLOWED_HOSTS(self): 24 | allowed_hosts = super().ALLOWED_HOSTS[:] 25 | allowed_hosts.append('test3') 26 | return allowed_hosts 27 | -------------------------------------------------------------------------------- /tests/settings/multiple_inheritance.py: -------------------------------------------------------------------------------- 1 | from .single_inheritance import Inheritance as BaseInheritance 2 | 3 | 4 | class Inheritance(BaseInheritance): 5 | 6 | def ALLOWED_HOSTS(self): 7 | allowed_hosts = super().ALLOWED_HOSTS[:] 8 | allowed_hosts.append('test-test') 9 | return allowed_hosts 10 | -------------------------------------------------------------------------------- /tests/settings/single_inheritance.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Inheritance(Base): 5 | 6 | @property 7 | def ALLOWED_HOSTS(self): 8 | allowed_hosts = super().ALLOWED_HOSTS[:] 9 | allowed_hosts.append('test') 10 | return allowed_hosts 11 | -------------------------------------------------------------------------------- /tests/setup_test.py: -------------------------------------------------------------------------------- 1 | """Used by tests to ensure logging is kept when calling setup() twice.""" 2 | 3 | from unittest import mock 4 | 5 | import configurations 6 | 7 | print('setup_1') 8 | configurations.setup() 9 | 10 | with mock.patch('django.setup', side_effect=Exception('setup called twice')): 11 | print('setup_2') 12 | configurations.setup() 13 | 14 | print('setup_done') 15 | -------------------------------------------------------------------------------- /tests/test_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.test import TestCase 3 | from unittest.mock import patch 4 | 5 | 6 | class DotEnvLoadingTests(TestCase): 7 | 8 | @patch.dict(os.environ, clear=True, 9 | DJANGO_CONFIGURATION='DotEnvConfiguration', 10 | DJANGO_SETTINGS_MODULE='tests.settings.dot_env') 11 | def test_env_loaded(self): 12 | from tests.settings import dot_env 13 | self.assertEqual(dot_env.DOTENV_VALUE, 'is set') 14 | self.assertEqual(dot_env.DOTENV_VALUE_METHOD, 'is set') 15 | self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV) 16 | -------------------------------------------------------------------------------- /tests/test_error.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.test import TestCase 3 | from unittest.mock import patch 4 | 5 | 6 | class ErrorTests(TestCase): 7 | 8 | @patch.dict(os.environ, clear=True, 9 | DJANGO_CONFIGURATION='ErrorConfiguration', 10 | DJANGO_SETTINGS_MODULE='tests.settings.error') 11 | def test_env_loaded(self): 12 | with self.assertRaises(ValueError) as cm: 13 | from tests.settings import error # noqa: F401 14 | 15 | self.assertIsInstance(cm.exception, ValueError) 16 | self.assertEqual( 17 | cm.exception.args, 18 | ( 19 | "Couldn't setup configuration " 20 | "'tests.settings.error.ErrorConfiguration': Error in pre_setup ", 21 | ) 22 | ) 23 | -------------------------------------------------------------------------------- /tests/test_inheritance.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase 4 | 5 | from unittest.mock import patch 6 | 7 | 8 | class InheritanceTests(TestCase): 9 | 10 | @patch.dict(os.environ, clear=True, 11 | DJANGO_CONFIGURATION='Inheritance', 12 | DJANGO_SETTINGS_MODULE='tests.settings.single_inheritance') 13 | def test_inherited(self): 14 | from tests.settings import single_inheritance 15 | self.assertEqual( 16 | single_inheritance.ALLOWED_HOSTS, 17 | ['test'] 18 | ) 19 | 20 | @patch.dict(os.environ, clear=True, 21 | DJANGO_CONFIGURATION='Inheritance', 22 | DJANGO_SETTINGS_MODULE='tests.settings.multiple_inheritance') 23 | def test_inherited2(self): 24 | from tests.settings import multiple_inheritance 25 | self.assertEqual( 26 | multiple_inheritance.ALLOWED_HOSTS, 27 | ['test', 'test-test'] 28 | ) 29 | 30 | @patch.dict(os.environ, clear=True, 31 | DJANGO_CONFIGURATION='Inheritance', 32 | DJANGO_SETTINGS_MODULE='tests.settings.mixin_inheritance') 33 | def test_inherited3(self): 34 | from tests.settings import mixin_inheritance 35 | self.assertEqual( 36 | mixin_inheritance.ALLOWED_HOSTS, 37 | ['test1', 'test2', 'test3'] 38 | ) 39 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | 5 | from django.test import TestCase 6 | from django.core.exceptions import ImproperlyConfigured 7 | 8 | from unittest.mock import patch 9 | 10 | from configurations.importer import ConfigurationFinder 11 | 12 | ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) 13 | TEST_PROJECT_DIR = os.path.join(ROOT_DIR, 'test_project') 14 | 15 | 16 | class MainTests(TestCase): 17 | 18 | def test_simple(self): 19 | from tests.settings import main 20 | self.assertEqual(main.ATTRIBUTE_SETTING, True) 21 | self.assertEqual(main.PROPERTY_SETTING, 1) 22 | self.assertEqual(main.METHOD_SETTING, 2) 23 | self.assertEqual(main.LAMBDA_SETTING, 3) 24 | self.assertNotEqual(main.PRISTINE_LAMBDA_SETTING, 4) 25 | self.assertTrue(lambda: callable(main.PRISTINE_LAMBDA_SETTING)) 26 | self.assertNotEqual(main.PRISTINE_FUNCTION_SETTING, 5) 27 | self.assertTrue(lambda: callable(main.PRISTINE_FUNCTION_SETTING)) 28 | self.assertEqual(main.ALLOWED_HOSTS, ['base']) 29 | self.assertEqual(main.PRE_SETUP_TEST_SETTING, 6) 30 | self.assertRaises(AttributeError, lambda: main.POST_SETUP_TEST_SETTING) 31 | self.assertEqual(main.Test.POST_SETUP_TEST_SETTING, 7) 32 | 33 | def test_global_arrival(self): 34 | from django.conf import settings 35 | self.assertEqual(settings.PROPERTY_SETTING, 1) 36 | self.assertRaises(AttributeError, lambda: settings._PRIVATE_SETTING) 37 | self.assertNotEqual(settings.PRISTINE_LAMBDA_SETTING, 4) 38 | self.assertTrue(lambda: callable(settings.PRISTINE_LAMBDA_SETTING)) 39 | self.assertNotEqual(settings.PRISTINE_FUNCTION_SETTING, 5) 40 | self.assertTrue(lambda: callable(settings.PRISTINE_FUNCTION_SETTING)) 41 | self.assertEqual(settings.PRE_SETUP_TEST_SETTING, 6) 42 | 43 | @patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test') 44 | def test_empty_module_var(self): 45 | with self.assertRaises(ImproperlyConfigured): 46 | ConfigurationFinder() 47 | 48 | @patch.dict(os.environ, clear=True, 49 | DJANGO_SETTINGS_MODULE='tests.settings.main') 50 | def test_empty_class_var(self): 51 | with self.assertRaises(ImproperlyConfigured): 52 | ConfigurationFinder() 53 | 54 | def test_global_settings(self): 55 | from configurations.base import Configuration 56 | self.assertIn('dictConfig', Configuration.LOGGING_CONFIG) 57 | self.assertEqual(repr(Configuration), 58 | "") 59 | 60 | def test_deprecated_settings_but_set_by_user(self): 61 | from tests.settings.main import TestWithDefaultSetExplicitely 62 | TestWithDefaultSetExplicitely.setup() 63 | self.assertEqual(TestWithDefaultSetExplicitely.DEFAULT_AUTO_FIELD, 64 | "django.db.models.BigAutoField") 65 | 66 | def test_repr(self): 67 | from tests.settings.main import Test 68 | self.assertEqual(repr(Test), 69 | "") 70 | 71 | @patch.dict(os.environ, clear=True, 72 | DJANGO_SETTINGS_MODULE='tests.settings.main', 73 | DJANGO_CONFIGURATION='Test') 74 | def test_initialization(self): 75 | finder = ConfigurationFinder() 76 | self.assertEqual(finder.module, 'tests.settings.main') 77 | self.assertEqual(finder.name, 'Test') 78 | self.assertEqual( 79 | repr(finder), 80 | "") 81 | 82 | @patch.dict(os.environ, clear=True, 83 | DJANGO_SETTINGS_MODULE='tests.settings.inheritance', 84 | DJANGO_CONFIGURATION='Inheritance') 85 | def test_initialization_inheritance(self): 86 | finder = ConfigurationFinder() 87 | self.assertEqual(finder.module, 88 | 'tests.settings.inheritance') 89 | self.assertEqual(finder.name, 'Inheritance') 90 | 91 | @patch.dict(os.environ, clear=True, 92 | DJANGO_SETTINGS_MODULE='tests.settings.main', 93 | DJANGO_CONFIGURATION='NonExisting') 94 | @patch.object(sys, 'argv', ['python', 'manage.py', 'test', 95 | '--settings=tests.settings.main', 96 | '--configuration=Test']) 97 | def test_configuration_option(self): 98 | finder = ConfigurationFinder(check_options=False) 99 | self.assertEqual(finder.module, 'tests.settings.main') 100 | self.assertEqual(finder.name, 'NonExisting') 101 | finder = ConfigurationFinder(check_options=True) 102 | self.assertEqual(finder.module, 'tests.settings.main') 103 | self.assertEqual(finder.name, 'Test') 104 | 105 | def test_configuration_argument_in_cli(self): 106 | """ 107 | Verify that's configuration option has been added to managements 108 | commands 109 | """ 110 | proc = subprocess.Popen(['django-cadmin', 'test', '--help'], 111 | stdout=subprocess.PIPE) 112 | self.assertIn('--configuration', proc.communicate()[0].decode('utf-8')) 113 | proc = subprocess.Popen(['django-cadmin', 'runserver', '--help'], 114 | stdout=subprocess.PIPE) 115 | self.assertIn('--configuration', proc.communicate()[0].decode('utf-8')) 116 | 117 | def test_configuration_argument_in_runypy_cli(self): 118 | """ 119 | Verify that's configuration option has been added to managements 120 | commands when using the -m entry point 121 | """ 122 | proc = subprocess.Popen( 123 | [sys.executable, '-m', 'configurations', 'test', '--help'], 124 | stdout=subprocess.PIPE, 125 | ) 126 | self.assertIn('--configuration', proc.communicate()[0].decode('utf-8')) 127 | proc = subprocess.Popen( 128 | [sys.executable, '-m', 'configurations', 'runserver', '--help'], 129 | stdout=subprocess.PIPE, 130 | ) 131 | self.assertIn('--configuration', proc.communicate()[0].decode('utf-8')) 132 | 133 | def test_django_setup_only_called_once(self): 134 | proc = subprocess.Popen( 135 | [sys.executable, os.path.join(os.path.dirname(__file__), 136 | 'setup_test.py')], 137 | stdout=subprocess.PIPE) 138 | res = proc.communicate() 139 | stdout = res[0].decode('utf-8') 140 | 141 | self.assertIn('setup_1', stdout) 142 | self.assertIn('setup_2', stdout) 143 | self.assertIn('setup_done', stdout) 144 | self.assertEqual(proc.returncode, 0) 145 | 146 | def test_utils_reraise(self): 147 | from configurations.utils import reraise 148 | 149 | class CustomException(Exception): 150 | pass 151 | 152 | with self.assertRaises(CustomException) as cm: 153 | try: 154 | raise CustomException 155 | except Exception as exc: 156 | reraise(exc, "Couldn't setup configuration", None) 157 | 158 | self.assertEqual(cm.exception.args, ("Couldn't setup configuration: ",)) 159 | -------------------------------------------------------------------------------- /tests/test_values.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | import os 3 | from contextlib import contextmanager 4 | 5 | from django import VERSION as DJANGO_VERSION 6 | from django.test import TestCase 7 | from django.core.exceptions import ImproperlyConfigured 8 | 9 | from unittest.mock import patch 10 | 11 | from configurations.values import (Value, BooleanValue, IntegerValue, 12 | FloatValue, DecimalValue, ListValue, 13 | TupleValue, SingleNestedTupleValue, 14 | SingleNestedListValue, SetValue, 15 | DictValue, URLValue, EmailValue, IPValue, 16 | RegexValue, PathValue, SecretValue, 17 | DatabaseURLValue, EmailURLValue, 18 | CacheURLValue, BackendsValue, 19 | CastingMixin, SearchURLValue, 20 | setup_value, PositiveIntegerValue) 21 | 22 | 23 | @contextmanager 24 | def env(**kwargs): 25 | with patch.dict(os.environ, clear=True, **kwargs): 26 | yield 27 | 28 | 29 | class FailingCasterValue(CastingMixin, Value): 30 | caster = 'non.existing.caster' 31 | 32 | 33 | class ValueTests(TestCase): 34 | 35 | def test_value_with_default(self): 36 | value = Value('default', environ=False) 37 | self.assertEqual(type(value), str) 38 | self.assertEqual(value, 'default') 39 | self.assertEqual(str(value), 'default') 40 | 41 | def test_value_with_default_and_late_binding(self): 42 | value = Value('default', environ=False, late_binding=True) 43 | self.assertEqual(type(value), Value) 44 | with env(DJANGO_TEST='override'): 45 | self.assertEqual(value.setup('TEST'), 'default') 46 | value = Value(environ_name='TEST') 47 | self.assertEqual(type(value), str) 48 | self.assertEqual(value, 'override') 49 | self.assertEqual(str(value), 'override') 50 | self.assertEqual(f'{value}', 'override') 51 | self.assertEqual('%s' % value, 'override') 52 | 53 | value = Value(environ_name='TEST', late_binding=True) 54 | self.assertEqual(type(value), Value) 55 | self.assertEqual(value.value, 'override') 56 | self.assertEqual(str(value), 'override') 57 | self.assertEqual(f'{value}', 'override') 58 | self.assertEqual('%s' % value, 'override') 59 | 60 | self.assertEqual(repr(value), repr('override')) 61 | 62 | def test_value_truthy(self): 63 | value = Value('default') 64 | self.assertTrue(bool(value)) 65 | 66 | def test_value_falsey(self): 67 | value = Value() 68 | self.assertFalse(bool(value)) 69 | 70 | @patch.dict(os.environ, clear=True, DJANGO_TEST='override') 71 | def test_env_var(self): 72 | value = Value('default') 73 | self.assertEqual(value.setup('TEST'), 'override') 74 | self.assertEqual(str(value), 'override') 75 | self.assertNotEqual(value.setup('TEST'), value.default) 76 | self.assertEqual(value.to_python(os.environ['DJANGO_TEST']), 77 | value.setup('TEST')) 78 | 79 | def test_value_reuse(self): 80 | value1 = Value('default') 81 | value2 = Value(value1) 82 | self.assertEqual(value1.setup('TEST1'), 'default') 83 | self.assertEqual(value2.setup('TEST2'), 'default') 84 | with env(DJANGO_TEST1='override1', DJANGO_TEST2='override2'): 85 | self.assertEqual(value1.setup('TEST1'), 'override1') 86 | self.assertEqual(value2.setup('TEST2'), 'override2') 87 | 88 | def test_value_var_equal(self): 89 | value1 = Value('default') 90 | value2 = Value('default') 91 | self.assertEqual(value1, value2) 92 | self.assertTrue(value1 in ['default']) 93 | 94 | def test_env_var_prefix(self): 95 | with patch.dict(os.environ, clear=True, ACME_TEST='override'): 96 | value = Value('default', environ_prefix='ACME') 97 | self.assertEqual(value.setup('TEST'), 'override') 98 | 99 | with patch.dict(os.environ, clear=True, TEST='override'): 100 | value = Value('default', environ_prefix='') 101 | self.assertEqual(value.setup('TEST'), 'override') 102 | 103 | with patch.dict(os.environ, clear=True, ACME_TEST='override'): 104 | value = Value('default', environ_prefix='ACME_') 105 | self.assertEqual(value.setup('TEST'), 'override') 106 | 107 | def test_boolean_values_true(self): 108 | value = BooleanValue(False) 109 | for truthy in value.true_values: 110 | with env(DJANGO_TEST=truthy): 111 | self.assertTrue(bool(value.setup('TEST'))) 112 | 113 | def test_boolean_values_faulty(self): 114 | self.assertRaises(ValueError, BooleanValue, 'false') 115 | 116 | def test_boolean_values_false(self): 117 | value = BooleanValue(True) 118 | for falsy in value.false_values: 119 | with env(DJANGO_TEST=falsy): 120 | self.assertFalse(bool(value.setup('TEST'))) 121 | 122 | def test_boolean_values_nonboolean(self): 123 | value = BooleanValue(True) 124 | with env(DJANGO_TEST='nonboolean'): 125 | self.assertRaises(ValueError, value.setup, 'TEST') 126 | 127 | def test_boolean_values_assign_false_to_another_booleanvalue(self): 128 | value1 = BooleanValue(False) 129 | value2 = BooleanValue(value1) 130 | self.assertFalse(value1.setup('TEST1')) 131 | self.assertFalse(value2.setup('TEST2')) 132 | 133 | def test_integer_values(self): 134 | value = IntegerValue(1) 135 | with env(DJANGO_TEST='2'): 136 | self.assertEqual(value.setup('TEST'), 2) 137 | with env(DJANGO_TEST='noninteger'): 138 | self.assertRaises(ValueError, value.setup, 'TEST') 139 | 140 | def test_positive_integer_values(self): 141 | value = PositiveIntegerValue(1) 142 | with env(DJANGO_TEST='2'): 143 | self.assertEqual(value.setup('TEST'), 2) 144 | with env(DJANGO_TEST='noninteger'): 145 | self.assertRaises(ValueError, value.setup, 'TEST') 146 | with env(DJANGO_TEST='-1'): 147 | self.assertRaises(ValueError, value.setup, 'TEST') 148 | 149 | def test_float_values(self): 150 | value = FloatValue(1.0) 151 | with env(DJANGO_TEST='2.0'): 152 | self.assertEqual(value.setup('TEST'), 2.0) 153 | with env(DJANGO_TEST='noninteger'): 154 | self.assertRaises(ValueError, value.setup, 'TEST') 155 | 156 | def test_decimal_values(self): 157 | value = DecimalValue(decimal.Decimal(1)) 158 | with env(DJANGO_TEST='2'): 159 | self.assertEqual(value.setup('TEST'), decimal.Decimal(2)) 160 | with env(DJANGO_TEST='nondecimal'): 161 | self.assertRaises(ValueError, value.setup, 'TEST') 162 | 163 | def test_failing_caster(self): 164 | self.assertRaises(ImproperlyConfigured, FailingCasterValue) 165 | 166 | def test_list_values_default(self): 167 | value = ListValue() 168 | with env(DJANGO_TEST='2,2'): 169 | self.assertEqual(value.setup('TEST'), ['2', '2']) 170 | with env(DJANGO_TEST='2, 2 ,'): 171 | self.assertEqual(value.setup('TEST'), ['2', '2']) 172 | with env(DJANGO_TEST=''): 173 | self.assertEqual(value.setup('TEST'), []) 174 | 175 | def test_list_values_separator(self): 176 | value = ListValue(separator=':') 177 | with env(DJANGO_TEST='/usr/bin:/usr/sbin:/usr/local/bin'): 178 | self.assertEqual(value.setup('TEST'), 179 | ['/usr/bin', '/usr/sbin', '/usr/local/bin']) 180 | 181 | def test_List_values_converter(self): 182 | value = ListValue(converter=int) 183 | with env(DJANGO_TEST='2,2'): 184 | self.assertEqual(value.setup('TEST'), [2, 2]) 185 | 186 | value = ListValue(converter=float) 187 | with env(DJANGO_TEST='2,2'): 188 | self.assertEqual(value.setup('TEST'), [2.0, 2.0]) 189 | 190 | def test_list_values_custom_converter(self): 191 | value = ListValue(converter=lambda x: x * 2) 192 | with env(DJANGO_TEST='2,2'): 193 | self.assertEqual(value.setup('TEST'), ['22', '22']) 194 | 195 | def test_list_values_converter_exception(self): 196 | value = ListValue(converter=int) 197 | with env(DJANGO_TEST='2,b'): 198 | self.assertRaises(ValueError, value.setup, 'TEST') 199 | 200 | def test_tuple_values_default(self): 201 | value = TupleValue() 202 | with env(DJANGO_TEST='2,2'): 203 | self.assertEqual(value.setup('TEST'), ('2', '2')) 204 | with env(DJANGO_TEST='2, 2 ,'): 205 | self.assertEqual(value.setup('TEST'), ('2', '2')) 206 | with env(DJANGO_TEST=''): 207 | self.assertEqual(value.setup('TEST'), ()) 208 | 209 | def test_single_nested_list_values_default(self): 210 | value = SingleNestedListValue() 211 | with env(DJANGO_TEST='2,3;4,5'): 212 | expected = [['2', '3'], ['4', '5']] 213 | self.assertEqual(value.setup('TEST'), expected) 214 | with env(DJANGO_TEST='2;3;4;5'): 215 | expected = [['2'], ['3'], ['4'], ['5']] 216 | self.assertEqual(value.setup('TEST'), expected) 217 | with env(DJANGO_TEST='2,3,4,5'): 218 | expected = [['2', '3', '4', '5']] 219 | self.assertEqual(value.setup('TEST'), expected) 220 | with env(DJANGO_TEST='2, 3 , ; 4 , 5 ; '): 221 | expected = [['2', '3'], ['4', '5']] 222 | self.assertEqual(value.setup('TEST'), expected) 223 | with env(DJANGO_TEST=''): 224 | self.assertEqual(value.setup('TEST'), []) 225 | 226 | def test_single_nested_list_values_separator(self): 227 | value = SingleNestedListValue(seq_separator=':') 228 | with env(DJANGO_TEST='2,3:4,5'): 229 | self.assertEqual(value.setup('TEST'), [['2', '3'], ['4', '5']]) 230 | 231 | def test_single_nested_list_values_converter(self): 232 | value = SingleNestedListValue(converter=int) 233 | with env(DJANGO_TEST='2,3;4,5'): 234 | self.assertEqual(value.setup('TEST'), [[2, 3], [4, 5]]) 235 | 236 | def test_single_nested_list_values_converter_default(self): 237 | value = SingleNestedListValue([['2', '3'], ['4', '5']], converter=int) 238 | self.assertEqual(value.value, [[2, 3], [4, 5]]) 239 | 240 | def test_single_nested_tuple_values_default(self): 241 | value = SingleNestedTupleValue() 242 | with env(DJANGO_TEST='2,3;4,5'): 243 | expected = (('2', '3'), ('4', '5')) 244 | self.assertEqual(value.setup('TEST'), expected) 245 | with env(DJANGO_TEST='2;3;4;5'): 246 | expected = (('2',), ('3',), ('4',), ('5',)) 247 | self.assertEqual(value.setup('TEST'), expected) 248 | with env(DJANGO_TEST='2,3,4,5'): 249 | expected = (('2', '3', '4', '5'),) 250 | self.assertEqual(value.setup('TEST'), expected) 251 | with env(DJANGO_TEST='2, 3 , ; 4 , 5 ; '): 252 | expected = (('2', '3'), ('4', '5')) 253 | self.assertEqual(value.setup('TEST'), expected) 254 | with env(DJANGO_TEST=''): 255 | self.assertEqual(value.setup('TEST'), ()) 256 | 257 | def test_single_nested_tuple_values_separator(self): 258 | value = SingleNestedTupleValue(seq_separator=':') 259 | with env(DJANGO_TEST='2,3:4,5'): 260 | self.assertEqual(value.setup('TEST'), (('2', '3'), ('4', '5'))) 261 | 262 | def test_single_nested_tuple_values_converter(self): 263 | value = SingleNestedTupleValue(converter=int) 264 | with env(DJANGO_TEST='2,3;4,5'): 265 | self.assertEqual(value.setup('TEST'), ((2, 3), (4, 5))) 266 | 267 | def test_single_nested_tuple_values_converter_default(self): 268 | value = SingleNestedTupleValue((('2', '3'), ('4', '5')), converter=int) 269 | self.assertEqual(value.value, ((2, 3), (4, 5))) 270 | 271 | def test_set_values_default(self): 272 | value = SetValue() 273 | with env(DJANGO_TEST='2,2'): 274 | self.assertEqual(value.setup('TEST'), {'2', '2'}) 275 | with env(DJANGO_TEST='2, 2 ,'): 276 | self.assertEqual(value.setup('TEST'), {'2', '2'}) 277 | with env(DJANGO_TEST=''): 278 | self.assertEqual(value.setup('TEST'), set()) 279 | 280 | def test_dict_values_default(self): 281 | value = DictValue() 282 | with env(DJANGO_TEST='{2: 2}'): 283 | self.assertEqual(value.setup('TEST'), {2: 2}) 284 | expected = {2: 2, '3': '3', '4': [1, 2, 3]} 285 | with env(DJANGO_TEST="{2: 2, '3': '3', '4': [1, 2, 3]}"): 286 | self.assertEqual(value.setup('TEST'), expected) 287 | with env(DJANGO_TEST="""{ 288 | 2: 2, 289 | '3': '3', 290 | '4': [1, 2, 3], 291 | }"""): 292 | self.assertEqual(value.setup('TEST'), expected) 293 | with env(DJANGO_TEST=''): 294 | self.assertEqual(value.setup('TEST'), {}) 295 | with env(DJANGO_TEST='spam'): 296 | self.assertRaises(ValueError, value.setup, 'TEST') 297 | 298 | def test_email_values(self): 299 | value = EmailValue('spam@eg.gs') 300 | with env(DJANGO_TEST='spam@sp.am'): 301 | self.assertEqual(value.setup('TEST'), 'spam@sp.am') 302 | with env(DJANGO_TEST='spam'): 303 | self.assertRaises(ValueError, value.setup, 'TEST') 304 | 305 | def test_url_values(self): 306 | value = URLValue('http://eggs.spam') 307 | with env(DJANGO_TEST='http://spam.eggs'): 308 | self.assertEqual(value.setup('TEST'), 'http://spam.eggs') 309 | with env(DJANGO_TEST='httb://spam.eggs'): 310 | self.assertRaises(ValueError, value.setup, 'TEST') 311 | 312 | def test_url_values_with_no_default(self): 313 | value = URLValue() # no default 314 | with env(DJANGO_TEST='http://spam.eggs'): 315 | self.assertEqual(value.setup('TEST'), 'http://spam.eggs') 316 | 317 | def test_url_values_with_wrong_default(self): 318 | self.assertRaises(ValueError, URLValue, 'httb://spam.eggs') 319 | 320 | def test_ip_values(self): 321 | value = IPValue('0.0.0.0') 322 | with env(DJANGO_TEST='127.0.0.1'): 323 | self.assertEqual(value.setup('TEST'), '127.0.0.1') 324 | with env(DJANGO_TEST='::1'): 325 | self.assertEqual(value.setup('TEST'), '::1') 326 | with env(DJANGO_TEST='spam.eggs'): 327 | self.assertRaises(ValueError, value.setup, 'TEST') 328 | 329 | def test_regex_values(self): 330 | value = RegexValue('000--000', regex=r'\d+--\d+') 331 | with env(DJANGO_TEST='123--456'): 332 | self.assertEqual(value.setup('TEST'), '123--456') 333 | with env(DJANGO_TEST='123456'): 334 | self.assertRaises(ValueError, value.setup, 'TEST') 335 | 336 | def test_path_values_with_check(self): 337 | value = PathValue() 338 | with env(DJANGO_TEST='/'): 339 | self.assertEqual(value.setup('TEST'), '/') 340 | with env(DJANGO_TEST='~/'): 341 | self.assertEqual(value.setup('TEST'), os.path.expanduser('~')) 342 | with env(DJANGO_TEST='/does/not/exist'): 343 | self.assertRaises(ValueError, value.setup, 'TEST') 344 | 345 | def test_path_values_no_check(self): 346 | value = PathValue(check_exists=False) 347 | with env(DJANGO_TEST='/'): 348 | self.assertEqual(value.setup('TEST'), '/') 349 | with env(DJANGO_TEST='~/spam/eggs'): 350 | self.assertEqual(value.setup('TEST'), 351 | os.path.join(os.path.expanduser('~'), 352 | 'spam', 'eggs')) 353 | with env(DJANGO_TEST='/does/not/exist'): 354 | self.assertEqual(value.setup('TEST'), '/does/not/exist') 355 | 356 | def test_secret_value(self): 357 | # no default allowed, only environment values are 358 | self.assertRaises(ValueError, SecretValue, 'default') 359 | 360 | value = SecretValue() 361 | self.assertRaises(ValueError, value.setup, 'TEST') 362 | with env(DJANGO_SECRET_KEY='123'): 363 | self.assertEqual(value.setup('SECRET_KEY'), '123') 364 | 365 | value = SecretValue(environ_name='FACEBOOK_API_SECRET', 366 | environ_prefix=None, 367 | late_binding=True) 368 | self.assertRaises(ValueError, value.setup, 'TEST') 369 | with env(FACEBOOK_API_SECRET='123'): 370 | self.assertEqual(value.setup('TEST'), '123') 371 | 372 | def test_database_url_value(self): 373 | value = DatabaseURLValue() 374 | self.assertEqual(value.default, {}) 375 | with env(DATABASE_URL='sqlite://'): 376 | settings_value = value.setup('DATABASE_URL') 377 | # Compare the embedded dicts in the "default" entry so that the difference can be seen if 378 | # it fails ... DatabaseURLValue(|) uses an external app that can add additional entries 379 | self.assertDictEqual( 380 | { 381 | 'CONN_HEALTH_CHECKS': False, 382 | 'CONN_MAX_AGE': 0, 383 | 'DISABLE_SERVER_SIDE_CURSORS': False, 384 | 'ENGINE': 'django.db.backends.sqlite3', 385 | 'HOST': '', 386 | 'NAME': ':memory:', 387 | 'PASSWORD': '', 388 | 'PORT': '', 389 | 'USER': '', 390 | }, 391 | settings_value['default'] 392 | ) 393 | 394 | def test_database_url_additional_args(self): 395 | 396 | def mock_database_url_caster(self, url, engine=None): 397 | return {'URL': url, 'ENGINE': engine} 398 | 399 | with patch('configurations.values.DatabaseURLValue.caster', 400 | mock_database_url_caster): 401 | value = DatabaseURLValue( 402 | engine='django_mysqlpool.backends.mysqlpool') 403 | with env(DATABASE_URL='sqlite://'): 404 | self.assertEqual(value.setup('DATABASE_URL'), { 405 | 'default': { 406 | 'URL': 'sqlite://', 407 | 'ENGINE': 'django_mysqlpool.backends.mysqlpool' 408 | } 409 | }) 410 | 411 | def test_email_url_value(self): 412 | value = EmailURLValue() 413 | self.assertEqual(value.default, {}) 414 | with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:587'): # noqa: E501 415 | self.assertEqual(value.setup('EMAIL_URL'), { 416 | 'EMAIL_BACKEND': 'django.core.mail.backends.smtp.EmailBackend', 417 | 'EMAIL_FILE_PATH': '', 418 | 'EMAIL_HOST': 'smtp.example.com', 419 | 'EMAIL_HOST_PASSWORD': 'password', 420 | 'EMAIL_HOST_USER': 'user@domain.com', 421 | 'EMAIL_PORT': 587, 422 | 'EMAIL_TIMEOUT': None, 423 | 'EMAIL_USE_SSL': False, 424 | 'EMAIL_USE_TLS': True}) 425 | with env(EMAIL_URL='console://'): 426 | self.assertEqual(value.setup('EMAIL_URL'), { 427 | 'EMAIL_BACKEND': 'django.core.mail.backends.console.EmailBackend', # noqa: E501 428 | 'EMAIL_FILE_PATH': '', 429 | 'EMAIL_HOST': None, 430 | 'EMAIL_HOST_PASSWORD': None, 431 | 'EMAIL_HOST_USER': None, 432 | 'EMAIL_PORT': None, 433 | 'EMAIL_TIMEOUT': None, 434 | 'EMAIL_USE_SSL': False, 435 | 'EMAIL_USE_TLS': False}) 436 | with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:wrong'): # noqa: E501 437 | self.assertRaises(ValueError, value.setup, 'TEST') 438 | 439 | def test_cache_url_value(self): 440 | cache_setting = { 441 | 'default': { 442 | 'BACKEND': 'django_redis.cache.RedisCache' if DJANGO_VERSION < (4,) else 'django.core.cache.backends.redis.RedisCache', # noqa: E501 443 | 'LOCATION': 'redis://host:6379/1', 444 | } 445 | } 446 | cache_url = 'redis://user@host:6379/1' 447 | value = CacheURLValue(cache_url) 448 | self.assertEqual(value.default, cache_setting) 449 | value = CacheURLValue() 450 | self.assertEqual(value.default, {}) 451 | with env(CACHE_URL='redis://user@host:6379/1'): 452 | self.assertEqual(value.setup('CACHE_URL'), cache_setting) 453 | with env(CACHE_URL='wrong://user@host:port/1'): 454 | with self.assertRaises(Exception) as cm: 455 | value.setup('TEST') 456 | self.assertEqual(cm.exception.args[0], 'Unknown backend: "wrong"') 457 | with env(CACHE_URL='redis://user@host:port/1'): 458 | with self.assertRaises(ValueError) as cm: 459 | value.setup('TEST') 460 | self.assertEqual( 461 | cm.exception.args[0], 462 | "Cannot interpret cache URL value 'redis://user@host:port/1'") 463 | 464 | def test_search_url_value(self): 465 | value = SearchURLValue() 466 | self.assertEqual(value.default, {}) 467 | with env(SEARCH_URL='elasticsearch://127.0.0.1:9200/index'): 468 | self.assertEqual(value.setup('SEARCH_URL'), { 469 | 'default': { 470 | 'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine', # noqa: E501 471 | 'URL': 'http://127.0.0.1:9200', 472 | 'INDEX_NAME': 'index', 473 | }}) 474 | 475 | def test_backend_list_value(self): 476 | backends = ['django.middleware.common.CommonMiddleware'] 477 | value = BackendsValue(backends) 478 | self.assertEqual(value.setup('TEST'), backends) 479 | 480 | backends = ['non.existing.Backend'] 481 | self.assertRaises(ValueError, BackendsValue, backends) 482 | 483 | def test_tuple_value(self): 484 | value = TupleValue(None) 485 | self.assertEqual(value.default, ()) 486 | self.assertEqual(value.value, ()) 487 | 488 | value = TupleValue((1, 2)) 489 | self.assertEqual(value.default, (1, 2)) 490 | self.assertEqual(value.value, (1, 2)) 491 | 492 | def test_set_value(self): 493 | value = SetValue() 494 | self.assertEqual(value.default, set()) 495 | self.assertEqual(value.value, set()) 496 | 497 | value = SetValue([1, 2]) 498 | self.assertEqual(value.default, {1, 2}) 499 | self.assertEqual(value.value, {1, 2}) 500 | 501 | def test_setup_value(self): 502 | 503 | class Target: 504 | pass 505 | 506 | value = EmailURLValue() 507 | with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:587'): # noqa: E501 508 | setup_value(Target, 'EMAIL', value) 509 | self.assertEqual(Target.EMAIL, { 510 | 'EMAIL_BACKEND': 'django.core.mail.backends.smtp.EmailBackend', 511 | 'EMAIL_FILE_PATH': '', 512 | 'EMAIL_HOST': 'smtp.example.com', 513 | 'EMAIL_HOST_PASSWORD': 'password', 514 | 'EMAIL_HOST_USER': 'user@domain.com', 515 | 'EMAIL_PORT': 587, 516 | 'EMAIL_TIMEOUT': None, 517 | 'EMAIL_USE_SSL': False, 518 | 'EMAIL_USE_TLS': True 519 | }) 520 | self.assertEqual( 521 | Target.EMAIL_BACKEND, 522 | 'django.core.mail.backends.smtp.EmailBackend') 523 | self.assertEqual(Target.EMAIL_FILE_PATH, '') 524 | self.assertEqual(Target.EMAIL_HOST, 'smtp.example.com') 525 | self.assertEqual(Target.EMAIL_HOST_PASSWORD, 'password') 526 | self.assertEqual(Target.EMAIL_HOST_USER, 'user@domain.com') 527 | self.assertEqual(Target.EMAIL_PORT, 587) 528 | self.assertEqual(Target.EMAIL_USE_TLS, True) 529 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | urlpatterns = [ 2 | ] 3 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = true 3 | usedevelop = true 4 | minversion = 1.8 5 | envlist = 6 | py311-checkqa 7 | docs 8 | py{39}-dj{32,41,42} 9 | py{310,py310}-dj{32,41,42,50,main} 10 | py{311}-dj{41,42,50,51,main} 11 | py{312}-dj{50,51,main} 12 | py{313}-dj{50,51,main} 13 | 14 | [gh-actions] 15 | python = 16 | 3.9: py39 17 | 3.10: py310 18 | 3.11: py311,flake8,readme 19 | 3.12: py312 20 | 3.13: py313 21 | pypy-3.10: pypy310 22 | 23 | [testenv] 24 | usedevelop = true 25 | setenv = 26 | DJANGO_SETTINGS_MODULE = tests.settings.main 27 | DJANGO_CONFIGURATION = Test 28 | COVERAGE_PROCESS_START = {toxinidir}/setup.cfg 29 | deps = 30 | dj32: django~=3.2.9 31 | dj41: django~=4.1.3 32 | dj42: django~=4.2.0 33 | dj50: django~=5.0.0 34 | dj51: django~=5.1.0 35 | djmain: https://github.com/django/django/archive/main.tar.gz 36 | py312: setuptools 37 | py312: wheel 38 | py313: setuptools 39 | py313: wheel 40 | coverage 41 | coverage_enable_subprocess 42 | extras = testing 43 | commands = 44 | python --version 45 | {envbindir}/coverage run {envbindir}/django-cadmin test -v2 {posargs:tests} 46 | coverage combine . tests docs 47 | coverage report -m --skip-covered 48 | coverage xml 49 | 50 | [testenv:py311-checkqa] 51 | commands = 52 | flake8 {toxinidir} 53 | check-manifest -v 54 | python setup.py sdist 55 | twine check dist/* 56 | deps = 57 | flake8 58 | twine 59 | check-manifest 60 | 61 | [testenv:docs] 62 | setenv = 63 | deps = 64 | -r docs/requirements.txt 65 | commands = 66 | sphinx-build \ 67 | -b html \ 68 | -a \ 69 | -W \ 70 | -n \ 71 | docs \ 72 | docs/_build/html 73 | --------------------------------------------------------------------------------