Compare commits

..

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

56 changed files with 1239 additions and 1079 deletions

View file

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

View file

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

View file

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

3
.gitignore vendored
View file

@ -1,5 +1,4 @@
.coverage .coverage
coverage.xml
docs/_build docs/_build
*.egg-info *.egg-info
*.egg *.egg
@ -9,4 +8,4 @@ build/
htmlcov/ htmlcov/
*.pyc *.pyc
dist/ dist/
.eggs/ tests/docs/_build/

View file

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

View file

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

86
.travis.yml Normal file
View file

@ -0,0 +1,86 @@
language: python
sudo: false
cache: pip
matrix:
include:
- env: TOXENV=flake8-py27
python: 2.7
- env: TOXENV=flake8-py36
python: 3.6
- env: TOXENV=readme-py27
python: 2.7
- env: TOXENV=py27-dj18
python: 2.7
- env: TOXENV=py27-dj110
python: 2.7
- env: TOXENV=py27-dj111
python: 2.7
- env: TOXENV=py34-dj18
python: 3.4
- env: TOXENV=py34-dj110
python: 3.4
- env: TOXENV=py34-dj111
python: 3.4
- env: TOXENV=py35-dj18
python: 3.5
- env: TOXENV=py35-dj110
python: 3.5
- env: TOXENV=py35-dj111
python: 3.5
- env: TOXENV=py36-dj18
python: 3.6
- env: TOXENV=py36-dj110
python: 3.6
- env: TOXENV=py36-dj111
python: 3.6
- env: TOXENV=pypy-dj18
python: pypy
- env: TOXENV=pypy-dj110
python: pypy
- env: TOXENV=pypy-dj111
python: pypy
- env: TOXENV=py34-dj20
python: 3.4
- env: TOXENV=py34-djmaster
python: 3.4
- env: TOXENV=py35-dj20
python: 3.5
- env: TOXENV=py35-djmaster
python: 3.5
- env: TOXENV=py36-dj20
python: 3.6
- env: TOXENV=py36-dj21
python: 3.6
- env: TOXENV=py36-djmaster
python: 3.6
allow_failures:
- env: TOXENV=py34-djmaster
python: 3.4
- env: TOXENV=py35-djmaster
python: 3.5
- env: TOXENV=py36-djmaster
python: 3.6
install:
- pip install tox
script: tox -v
after_success:
- |
if [ "$TOXENV" = py* ]; then
pip install codecov
coverage xml
coverage report -m
codecov --required -X gcov fix pycov -f coverage.xml --flags ${TOXENV//-/ }
fi
branches:
except: templates/1.5.x templates/1.6.x
deploy:
provider: pypi
user: jazzband
server: https://jazzband.co/projects/django-configurations/upload
distributions: sdist bdist_wheel
password:
secure: LuserSjUTGSsls9zrvck/FbfL+gFpNU/ywOQ/67ufEbbpGCeDBEgxDzgb0acfHNk8wlAkaPvaAejQBFtcUulhdNT/g0NsmaEAjd6HhCGM+FRJAnYFaj33Js6C+N2tX5wznL7uCBxqgtaaH0hf6ucqC8OXqwoCVGgdxAEnUlC/fY=
on:
tags: true
repo: jazzband/django-configurations
condition: "$TOXENV = py36-dj111"

18
AUTHORS
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
from .base import Configuration # noqa # flake8: noqa
from .decorators import pristinemethod # noqa from .base import Configuration
from .version import __version__ # noqa from .decorators import pristinemethod
__version__ = '2.1'
__all__ = ['Configuration', 'pristinemethod'] __all__ = ['Configuration', 'pristinemethod']
@ -11,10 +11,8 @@ def _setup():
importer.install() importer.install()
from django.apps import apps import django
if not apps.ready: django.setup()
import django
django.setup()
def load_ipython_extension(ipython): def load_ipython_extension(ipython):

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import os import os
import re import re
from django.utils import six
from django.conf import global_settings from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -31,50 +32,15 @@ class ConfigurationBase(type):
if parents: if parents:
for base in bases[::-1]: for base in bases[::-1]:
settings_vars.update(uppercase_attributes(base)) settings_vars.update(uppercase_attributes(base))
attrs = dict(settings_vars, **attrs)
deprecated_settings = { return super(ConfigurationBase, cls).__new__(cls, name, bases, attrs)
# DEFAULT_HASHING_ALGORITHM is always deprecated, as it's a
# transitional setting
# https://docs.djangoproject.com/en/3.1/releases/3.1/#default-hashing-algorithm-settings
"DEFAULT_HASHING_ALGORITHM",
# DEFAULT_CONTENT_TYPE and FILE_CHARSET are deprecated in
# Django 2.2 and are removed in Django 3.0
"DEFAULT_CONTENT_TYPE",
"FILE_CHARSET",
# When DEFAULT_AUTO_FIELD is not explicitly set, Django's emits a
# system check warning models.W042. This warning should not be
# suppressed, as downstream users are expected to make a decision.
# https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys
"DEFAULT_AUTO_FIELD",
# FORMS_URLFIELD_ASSUME_HTTPS is a transitional setting introduced
# in Django 5.0.
# https://docs.djangoproject.com/en/5.0/releases/5.0/#id2
"FORMS_URLFIELD_ASSUME_HTTPS"
}
# PASSWORD_RESET_TIMEOUT_DAYS is deprecated in favor of
# PASSWORD_RESET_TIMEOUT in Django 3.1
# https://github.com/django/django/commit/226ebb17290b604ef29e82fb5c1fbac3594ac163#diff-ec2bed07bb264cb95a80f08d71a47c06R163-R170
if "PASSWORD_RESET_TIMEOUT" in settings_vars:
deprecated_settings.add("PASSWORD_RESET_TIMEOUT_DAYS")
# DEFAULT_FILE_STORAGE and STATICFILES_STORAGE are deprecated
# in favor of STORAGES.
# https://docs.djangoproject.com/en/dev/releases/4.2/#custom-file-storages
if "STORAGES" in settings_vars:
deprecated_settings.add("DEFAULT_FILE_STORAGE")
deprecated_settings.add("STATICFILES_STORAGE")
for deprecated_setting in deprecated_settings:
if deprecated_setting in settings_vars:
del settings_vars[deprecated_setting]
attrs = {**settings_vars, **attrs}
return super().__new__(cls, name, bases, attrs)
def __repr__(self): def __repr__(self):
return "<Configuration '{}.{}'>".format(self.__module__, return "<Configuration '{0}.{1}'>".format(self.__module__,
self.__name__) self.__name__)
class Configuration(metaclass=ConfigurationBase): class Configuration(six.with_metaclass(ConfigurationBase)):
""" """
The base configuration class to inherit from. The base configuration class to inherit from.
@ -107,10 +73,10 @@ class Configuration(metaclass=ConfigurationBase):
environment variables from a .env file located in the project root environment variables from a .env file located in the project root
or provided directory. or provided directory.
https://wellfire.co/learn/easier-12-factor-django/ http://www.wellfireinteractive.com/blog/easier-12-factor-django/
https://gist.github.com/bennylope/2999704 https://gist.github.com/bennylope/2999704
""" """
# check if the class has DOTENV set whether with a path or None # check if the class has DOTENV set wether with a path or None
dotenv = getattr(cls, 'DOTENV', None) dotenv = getattr(cls, 'DOTENV', None)
# if DOTENV is falsy we want to disable it # if DOTENV is falsy we want to disable it
@ -119,12 +85,12 @@ class Configuration(metaclass=ConfigurationBase):
# now check if we can access the file since we know we really want to # now check if we can access the file since we know we really want to
try: try:
with open(dotenv) as f: with open(dotenv, 'r') as f:
content = f.read() content = f.read()
except OSError as e: except IOError as e:
raise ImproperlyConfigured("Couldn't read .env file " raise ImproperlyConfigured("Couldn't read .env file "
"with the path {}. Error: " "with the path {}. Error: "
"{}".format(dotenv, e)) from e "{}".format(dotenv, e))
else: else:
for line in content.splitlines(): for line in content.splitlines():
m1 = re.match(r'\A([A-Za-z_0-9]+)=(.*)\Z', line) m1 = re.match(r'\A([A-Za-z_0-9]+)=(.*)\Z', line)

View file

@ -1,4 +1,4 @@
from importlib.machinery import PathFinder import imp
import logging import logging
import os import os
import sys import sys
@ -46,12 +46,12 @@ def install(check_options=False):
return parser return parser
base.BaseCommand.create_parser = create_parser base.BaseCommand.create_parser = create_parser
importer = ConfigurationFinder(check_options=check_options) importer = ConfigurationImporter(check_options=check_options)
sys.meta_path.insert(0, importer) sys.meta_path.insert(0, importer)
installed = True installed = True
class ConfigurationFinder(PathFinder): class ConfigurationImporter(object):
modvar = SETTINGS_ENVIRONMENT_VARIABLE modvar = SETTINGS_ENVIRONMENT_VARIABLE
namevar = CONFIGURATION_ENVIRONMENT_VARIABLE namevar = CONFIGURATION_ENVIRONMENT_VARIABLE
error_msg = ("Configuration cannot be imported, " error_msg = ("Configuration cannot be imported, "
@ -70,7 +70,7 @@ class ConfigurationFinder(PathFinder):
self.announce() self.announce()
def __repr__(self): def __repr__(self):
return "<ConfigurationFinder for '{}.{}'>".format(self.module, return "<ConfigurationImporter for '{0}.{1}'>".format(self.module,
self.name) self.name)
@property @property
@ -82,10 +82,16 @@ class ConfigurationFinder(PathFinder):
return os.environ.get(self.namevar) return os.environ.get(self.namevar)
def check_options(self): def check_options(self):
parser = base.CommandParser( try:
usage="%(prog)s subcommand [options] [args]", parser = base.CommandParser(
add_help=False, usage="%(prog)s subcommand [options] [args]",
) add_help=False)
except TypeError:
# Django before 2.1 used a `cmd` argument.
parser = base.CommandParser(
None,
usage="%(prog)s subcommand [options] [args]",
add_help=False)
parser.add_argument('--settings') parser.add_argument('--settings')
parser.add_argument('--pythonpath') parser.add_argument('--pythonpath')
parser.add_argument(CONFIGURATION_ARGUMENT, parser.add_argument(CONFIGURATION_ARGUMENT,
@ -118,63 +124,61 @@ class ConfigurationFinder(PathFinder):
def stylize(text): def stylize(text):
return colorize(text, fg='green') return colorize(text, fg='green')
if (self.argv[1] == 'runserver' if (self.argv[1] == 'runserver' and
and os.environ.get('RUN_MAIN') == 'true'): os.environ.get('RUN_MAIN') == 'true'):
message = ("django-configurations version {}, using " message = ("django-configurations version {0}, using "
"configuration {}".format(__version__ or "", "configuration '{1}'".format(__version__,
self.name)) self.name))
self.logger.debug(stylize(message)) self.logger.debug(stylize(message))
def find_spec(self, fullname, path=None, target=None): def find_module(self, fullname, path=None):
if fullname is not None and fullname == self.module: if fullname is not None and fullname == self.module:
spec = super().find_spec(fullname, path, target) module = fullname.rsplit('.', 1)[-1]
if spec is not None: return ConfigurationLoader(self.name,
wrap_loader(spec.loader, self.name) imp.find_module(module, path))
return spec return None
class ConfigurationLoader(object):
def __init__(self, name, location):
self.name = name
self.location = location
def load_module(self, fullname):
if fullname in sys.modules:
mod = sys.modules[fullname] # pragma: no cover
else: else:
return None mod = imp.load_module(fullname, *self.location)
cls_path = '{0}.{1}'.format(mod.__name__, self.name)
try:
cls = getattr(mod, self.name)
except AttributeError as err: # pragma: no cover
reraise(err, "Couldn't find configuration '{0}' "
"in module '{1}'".format(self.name,
mod.__package__))
try:
cls.pre_setup()
cls.setup()
obj = cls()
attributes = uppercase_attributes(obj).items()
for name, value in attributes:
if callable(value) and not getattr(value, 'pristine', False):
value = value()
# in case a method returns a Value instance we have
# to do the same as the Configuration.setup method
if isinstance(value, Value):
setup_value(mod, name, value)
continue
setattr(mod, name, value)
def wrap_loader(loader, class_name): setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(fullname,
class ConfigurationLoader(loader.__class__): self.name))
def exec_module(self, module): cls.post_setup()
super().exec_module(module)
mod = module except Exception as err:
reraise(err, "Couldn't setup configuration '{0}'".format(cls_path))
cls_path = f'{mod.__name__}.{class_name}' return mod
try:
cls = getattr(mod, class_name)
except AttributeError as err: # pragma: no cover
reraise(
err,
(
f"Couldn't find configuration '{class_name}' in "
f"module '{mod.__package__}'"
),
)
try:
cls.pre_setup()
cls.setup()
obj = cls()
attributes = uppercase_attributes(obj).items()
for name, value in attributes:
if callable(value) and not getattr(value, 'pristine', False):
value = value()
# in case a method returns a Value instance we have
# to do the same as the Configuration.setup method
if isinstance(value, Value):
setup_value(mod, name, value)
continue
setattr(mod, name, value)
setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(module.__name__,
class_name))
cls.post_setup()
except Exception as err:
reraise(err, f"Couldn't setup configuration '{cls_path}'")
loader.__class__ = ConfigurationLoader

View file

@ -1,11 +1,12 @@
import inspect import inspect
import sys import sys
import warnings
from functools import partial
from importlib import import_module
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils import six
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
def isuppercase(name): def isuppercase(name):
@ -13,7 +14,8 @@ def isuppercase(name):
def uppercase_attributes(obj): def uppercase_attributes(obj):
return {name: getattr(obj, name) for name in dir(obj) if isuppercase(name)} return dict((name, getattr(obj, name))
for name in filter(isuppercase, dir(obj)))
def import_by_path(dotted_path, error_prefix=''): def import_by_path(dotted_path, error_prefix=''):
@ -24,26 +26,25 @@ def import_by_path(dotted_path, error_prefix=''):
Backported from Django 1.6. Backported from Django 1.6.
""" """
warnings.warn("Function utils.import_by_path is deprecated in favor of "
"django.utils.module_loading.import_string.", DeprecationWarning)
try: try:
module_path, class_name = dotted_path.rsplit('.', 1) module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError: except ValueError:
raise ImproperlyConfigured("{}{} doesn't look like " raise ImproperlyConfigured("{0}{1} doesn't look like "
"a module path".format(error_prefix, "a module path".format(error_prefix,
dotted_path)) dotted_path))
try: try:
module = import_module(module_path) module = import_module(module_path)
except ImportError as err: except ImportError as err:
msg = '{}Error importing module {}: "{}"'.format(error_prefix, msg = '{0}Error importing module {1}: "{2}"'.format(error_prefix,
module_path, module_path,
err) err)
raise ImproperlyConfigured(msg).with_traceback(sys.exc_info()[2]) six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg),
sys.exc_info()[2])
try: try:
attr = getattr(module, class_name) attr = getattr(module, class_name)
except AttributeError: except AttributeError:
raise ImproperlyConfigured('{}Module "{}" does not define a ' raise ImproperlyConfigured('{0}Module "{1}" does not define a '
'"{}" attribute/class'.format(error_prefix, '"{2}" attribute/class'.format(error_prefix,
module_path, module_path,
class_name)) class_name))
return attr return attr
@ -61,41 +62,78 @@ def reraise(exc, prefix=None, suffix=None):
suffix = '' suffix = ''
elif not (suffix.startswith('(') and suffix.endswith(')')): elif not (suffix.startswith('(') and suffix.endswith(')')):
suffix = '(' + suffix + ')' suffix = '(' + suffix + ')'
exc.args = (f'{prefix} {args[0]} {suffix}',) + args[1:] exc.args = ('{0} {1} {2}'.format(prefix, exc.args[0], suffix),) + args[1:]
raise exc raise
# Copied over from Sphinx # Copied over from Sphinx
def getargspec(func): if sys.version_info >= (3, 0):
"""Like inspect.getargspec but supports functools.partial as well.""" from functools import partial
if inspect.ismethod(func):
func = func.__func__ def getargspec(func):
if type(func) is partial: """Like inspect.getargspec but supports functools.partial as well."""
orig_func = func.func if inspect.ismethod(func):
argspec = getargspec(orig_func) func = func.__func__
args = list(argspec[0]) if type(func) is partial:
defaults = list(argspec[3] or ()) orig_func = func.func
kwoargs = list(argspec[4]) argspec = getargspec(orig_func)
kwodefs = dict(argspec[5] or {}) args = list(argspec[0])
if func.args: defaults = list(argspec[3] or ())
args = args[len(func.args):] kwoargs = list(argspec[4])
for arg in func.keywords or (): kwodefs = dict(argspec[5] or {})
try: if func.args:
args = args[len(func.args):]
for arg in func.keywords or ():
try:
i = args.index(arg) - len(args)
del args[i]
try:
del defaults[i]
except IndexError:
pass
except ValueError: # must be a kwonly arg
i = kwoargs.index(arg)
del kwoargs[i]
del kwodefs[arg]
return inspect.FullArgSpec(args, argspec[1], argspec[2],
tuple(defaults), kwoargs,
kwodefs, argspec[6])
while hasattr(func, '__wrapped__'):
func = func.__wrapped__
if not inspect.isfunction(func):
raise TypeError('%r is not a Python function' % func)
return inspect.getfullargspec(func)
else: # 2.6, 2.7
from functools import partial
def getargspec(func):
"""Like inspect.getargspec but supports functools.partial as well."""
if inspect.ismethod(func):
func = func.im_func
parts = 0, ()
if type(func) is partial:
keywords = func.keywords
if keywords is None:
keywords = {}
parts = len(func.args), keywords.keys()
func = func.func
if not inspect.isfunction(func):
raise TypeError('%r is not a Python function' % func)
args, varargs, varkw = inspect.getargs(func.func_code)
func_defaults = func.func_defaults
if func_defaults is None:
func_defaults = []
else:
func_defaults = list(func_defaults)
if parts[0]:
args = args[parts[0]:]
if parts[1]:
for arg in parts[1]:
i = args.index(arg) - len(args) i = args.index(arg) - len(args)
del args[i] del args[i]
try: try:
del defaults[i] del func_defaults[i]
except IndexError: except IndexError:
pass pass
except ValueError: # must be a kwonly arg return inspect.ArgSpec(args, varargs, varkw, func_defaults)
i = kwoargs.index(arg)
del kwoargs[i]
del kwodefs[arg]
return inspect.FullArgSpec(args, argspec[1], argspec[2],
tuple(defaults), kwoargs,
kwodefs, argspec[6])
while hasattr(func, '__wrapped__'):
func = func.__wrapped__
if not inspect.isfunction(func):
raise TypeError('%r is not a Python function' % func)
return inspect.getfullargspec(func)

View file

@ -6,9 +6,9 @@ import sys
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError, ImproperlyConfigured from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.module_loading import import_string from django.utils import six
from .utils import getargspec from .utils import import_by_path, getargspec
def setup_value(target, name, value): def setup_value(target, name, value):
@ -20,7 +20,7 @@ def setup_value(target, name, value):
setattr(target, multiple_name, multiple_value) setattr(target, multiple_name, multiple_value)
class Value: class Value(object):
""" """
A single settings value that is able to interpret env variables A single settings value that is able to interpret env variables
and implements a simple validation scheme. and implements a simple validation scheme.
@ -52,8 +52,8 @@ class Value:
instance.late_binding = kwargs.get('late_binding') instance.late_binding = kwargs.get('late_binding')
if not instance.late_binding: if not instance.late_binding:
instance.__init__(*args, **kwargs) instance.__init__(*args, **kwargs)
if ((instance.environ and instance.environ_name) if ((instance.environ and instance.environ_name) or
or (not instance.environ and instance.default)): (not instance.environ and instance.default)):
instance = instance.setup(instance.environ_name) instance = instance.setup(instance.environ_name)
return instance return instance
@ -92,7 +92,7 @@ class Value:
else: else:
environ_name = name.upper() environ_name = name.upper()
if self.environ_prefix: if self.environ_prefix:
environ_name = f'{self.environ_prefix}_{environ_name}' environ_name = '{0}_{1}'.format(self.environ_prefix, environ_name)
return environ_name return environ_name
def setup(self, name): def setup(self, name):
@ -102,8 +102,8 @@ class Value:
if full_environ_name in os.environ: if full_environ_name in os.environ:
value = self.to_python(os.environ[full_environ_name]) value = self.to_python(os.environ[full_environ_name])
elif self.environ_required: elif self.environ_required:
raise ValueError('Value {!r} is required to be set as the ' raise ValueError('Value {0!r} is required to be set as the '
'environment variable {!r}' 'environment variable {1!r}'
.format(name, full_environ_name)) .format(name, full_environ_name))
self.value = value self.value = value
return value return value
@ -112,12 +112,12 @@ class Value:
""" """
Convert the given value of a environment variable into an Convert the given value of a environment variable into an
appropriate Python representation of the value. appropriate Python representation of the value.
This should be overridden when subclassing. This should be overriden when subclassing.
""" """
return value return value
class MultipleMixin: class MultipleMixin(object):
multiple = True multiple = True
@ -126,9 +126,9 @@ class BooleanValue(Value):
false_values = ('no', 'n', 'false', '0', '') false_values = ('no', 'n', 'false', '0', '')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super(BooleanValue, self).__init__(*args, **kwargs)
if self.default not in (True, False): if self.default not in (True, False):
raise ValueError('Default value {!r} is not a ' raise ValueError('Default value {0!r} is not a '
'boolean value'.format(self.default)) 'boolean value'.format(self.default))
def to_python(self, value): def to_python(self, value):
@ -139,30 +139,28 @@ class BooleanValue(Value):
return False return False
else: else:
raise ValueError('Cannot interpret ' raise ValueError('Cannot interpret '
'boolean value {!r}'.format(value)) 'boolean value {0!r}'.format(value))
class CastingMixin: class CastingMixin(object):
exception = (TypeError, ValueError) exception = (TypeError, ValueError)
message = 'Cannot interpret value {0!r}' message = 'Cannot interpret value {0!r}'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super(CastingMixin, self).__init__(*args, **kwargs)
if isinstance(self.caster, str): if isinstance(self.caster, six.string_types):
try: self._caster = import_by_path(self.caster)
self._caster = import_string(self.caster)
except ImportError as err:
msg = f"Could not import {self.caster!r}"
raise ImproperlyConfigured(msg) from err
elif callable(self.caster): elif callable(self.caster):
self._caster = self.caster self._caster = self.caster
else: else:
error = 'Cannot use caster of {} ({!r})'.format(self, error = 'Cannot use caster of {0} ({1!r})'.format(self,
self.caster) self.caster)
raise ValueError(error) raise ValueError(error)
try: try:
arg_names = getargspec(self._caster)[0] arg_names = getargspec(self._caster)[0]
self._params = {name: kwargs[name] for name in arg_names if name in kwargs} self._params = dict((name, kwargs[name])
for name in arg_names
if name in kwargs)
except TypeError: except TypeError:
self._params = {} self._params = {}
@ -183,7 +181,7 @@ class IntegerValue(CastingMixin, Value):
class PositiveIntegerValue(IntegerValue): class PositiveIntegerValue(IntegerValue):
def to_python(self, value): def to_python(self, value):
int_value = super().to_python(value) int_value = super(PositiveIntegerValue, self).to_python(value)
if int_value < 0: if int_value < 0:
raise ValueError(self.message.format(value)) raise ValueError(self.message.format(value))
return int_value return int_value
@ -215,7 +213,7 @@ class SequenceValue(Value):
converter = kwargs.pop('converter', None) converter = kwargs.pop('converter', None)
if converter is not None: if converter is not None:
self.converter = converter self.converter = converter
super().__init__(*args, **kwargs) super(SequenceValue, self).__init__(*args, **kwargs)
# make sure the default is the correct sequence type # make sure the default is the correct sequence type
if self.default is None: if self.default is None:
self.default = self.sequence_type() self.default = self.sequence_type()
@ -259,7 +257,7 @@ class SingleNestedSequenceValue(SequenceValue):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.seq_separator = kwargs.pop('seq_separator', ';') self.seq_separator = kwargs.pop('seq_separator', ';')
super().__init__(*args, **kwargs) super(SingleNestedSequenceValue, self).__init__(*args, **kwargs)
def _convert(self, items): def _convert(self, items):
# This could receive either a bare or nested sequence # This could receive either a bare or nested sequence
@ -268,7 +266,8 @@ class SingleNestedSequenceValue(SequenceValue):
super(SingleNestedSequenceValue, self)._convert(i) for i in items super(SingleNestedSequenceValue, self)._convert(i) for i in items
] ]
return self.sequence_type(converted_sequences) return self.sequence_type(converted_sequences)
return self.sequence_type(super()._convert(items)) return self.sequence_type(
super(SingleNestedSequenceValue, self)._convert(items))
def to_python(self, value): def to_python(self, value):
split_value = [ split_value = [
@ -294,9 +293,9 @@ class BackendsValue(ListValue):
def converter(self, value): def converter(self, value):
try: try:
import_string(value) import_by_path(value)
except ImportError as err: except ImproperlyConfigured as err:
raise ValueError(err).with_traceback(sys.exc_info()[2]) six.reraise(ValueError, ValueError(err), sys.exc_info()[2])
return value return value
@ -304,28 +303,28 @@ class SetValue(ListValue):
message = 'Cannot interpret set item {0!r} in set {1!r}' message = 'Cannot interpret set item {0!r} in set {1!r}'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super(SetValue, self).__init__(*args, **kwargs)
if self.default is None: if self.default is None:
self.default = set() self.default = set()
else: else:
self.default = set(self.default) self.default = set(self.default)
def to_python(self, value): def to_python(self, value):
return set(super().to_python(value)) return set(super(SetValue, self).to_python(value))
class DictValue(Value): class DictValue(Value):
message = 'Cannot interpret dict value {0!r}' message = 'Cannot interpret dict value {0!r}'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super(DictValue, self).__init__(*args, **kwargs)
if self.default is None: if self.default is None:
self.default = {} self.default = {}
else: else:
self.default = dict(self.default) self.default = dict(self.default)
def to_python(self, value): def to_python(self, value):
value = super().to_python(value) value = super(DictValue, self).to_python(value)
if not value: if not value:
return {} return {}
try: try:
@ -337,21 +336,17 @@ class DictValue(Value):
return evaled_value return evaled_value
class ValidationMixin: class ValidationMixin(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super(ValidationMixin, self).__init__(*args, **kwargs)
if isinstance(self.validator, str): if isinstance(self.validator, six.string_types):
try: self._validator = import_by_path(self.validator)
self._validator = import_string(self.validator)
except ImportError as err:
msg = f"Could not import {self.validator!r}"
raise ImproperlyConfigured(msg) from err
elif callable(self.validator): elif callable(self.validator):
self._validator = self.validator self._validator = self.validator
else: else:
raise ValueError('Cannot use validator of ' raise ValueError('Cannot use validator of '
'{} ({!r})'.format(self, self.validator)) '{0} ({1!r})'.format(self, self.validator))
if self.default: if self.default:
self.to_python(self.default) self.to_python(self.default)
@ -385,19 +380,19 @@ class RegexValue(ValidationMixin, Value):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
regex = kwargs.pop('regex', None) regex = kwargs.pop('regex', None)
self.validator = validators.RegexValidator(regex=regex) self.validator = validators.RegexValidator(regex=regex)
super().__init__(*args, **kwargs) super(RegexValue, self).__init__(*args, **kwargs)
class PathValue(Value): class PathValue(Value):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.check_exists = kwargs.pop('check_exists', True) self.check_exists = kwargs.pop('check_exists', True)
super().__init__(*args, **kwargs) super(PathValue, self).__init__(*args, **kwargs)
def setup(self, name): def setup(self, name):
value = super().setup(name) value = super(PathValue, self).setup(name)
value = os.path.expanduser(value) value = os.path.expanduser(value)
if self.check_exists and not os.path.exists(value): if self.check_exists and not os.path.exists(value):
raise ValueError(f'Path {value!r} does not exist.') raise ValueError('Path {0!r} does not exist.'.format(value))
return os.path.abspath(value) return os.path.abspath(value)
@ -406,15 +401,15 @@ class SecretValue(Value):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['environ'] = True kwargs['environ'] = True
kwargs['environ_required'] = True kwargs['environ_required'] = True
super().__init__(*args, **kwargs) super(SecretValue, self).__init__(*args, **kwargs)
if self.default is not None: if self.default is not None:
raise ValueError('Secret values are only allowed to ' raise ValueError('Secret values are only allowed to '
'be set as environment variables') 'be set as environment variables')
def setup(self, name): def setup(self, name):
value = super().setup(name) value = super(SecretValue, self).setup(name)
if not value: if not value:
raise ValueError(f'Secret value {name!r} is not set') raise ValueError('Secret value {0!r} is not set'.format(name))
return value return value
@ -427,7 +422,7 @@ class EmailURLValue(CastingMixin, MultipleMixin, Value):
kwargs.setdefault('environ', True) kwargs.setdefault('environ', True)
kwargs.setdefault('environ_prefix', None) kwargs.setdefault('environ_prefix', None)
kwargs.setdefault('environ_name', 'EMAIL_URL') kwargs.setdefault('environ_name', 'EMAIL_URL')
super().__init__(*args, **kwargs) super(EmailURLValue, self).__init__(*args, **kwargs)
if self.default is None: if self.default is None:
self.default = {} self.default = {}
else: else:
@ -442,14 +437,14 @@ class DictBackendMixin(Value):
kwargs.setdefault('environ', True) kwargs.setdefault('environ', True)
kwargs.setdefault('environ_prefix', None) kwargs.setdefault('environ_prefix', None)
kwargs.setdefault('environ_name', self.environ_name) kwargs.setdefault('environ_name', self.environ_name)
super().__init__(*args, **kwargs) super(DictBackendMixin, self).__init__(*args, **kwargs)
if self.default is None: if self.default is None:
self.default = {} self.default = {}
else: else:
self.default = self.to_python(self.default) self.default = self.to_python(self.default)
def to_python(self, value): def to_python(self, value):
value = super().to_python(value) value = super(DictBackendMixin, self).to_python(value)
return {self.alias: value} return {self.alias: value}

View file

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

View file

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

153
docs/Makefile Normal file
View file

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

View file

@ -3,110 +3,6 @@
Changelog Changelog
--------- ---------
Unreleased
^^^^^^^^^^
- Prevent warning about ``FORMS_URLFIELD_ASSUME_HTTPS`` on Django 5.0.
v2.5.1 (2023-11-30)
^^^^^^^^^^^^^^^^^^^
- Add compatibility with Python 3.12
v2.5 (2023-10-20)
^^^^^^^^^^^^^^^^^
- Update Github actions and fix pipeline warnings
- Add compatibility with Django 5.0
- **BACKWARD INCOMPATIBLE** Drop compatibility for Django 4.0
- **BACKWARD INCOMPATIBLE** Drop compatibility for Python 3.7 and PyPy < 3.10
v2.4.2 (2023-09-27)
^^^^^^^^^^^^^^^^^^^
- Replace imp (due for removal in Python 3.12) with importlib
- Test on PyPy 3.10.
v2.4.1 (2023-04-04)
^^^^^^^^^^^^^^^^^^^
- Use furo as documentation theme
- Add compatibility with Django 4.2 - fix "STATICFILES_STORAGE/STORAGES are mutually exclusive" error.
- Test Django 4.1.3+ on Python 3.11
v2.4 (2022-08-24)
^^^^^^^^^^^^^^^^^
- Add compatibility with Django 4.1
- **BACKWARD INCOMPATIBLE** Drop compatibility for Django < 3.2
- **BACKWARD INCOMPATIBLE** Drop compatibility for Python 3.6
v2.3.2 (2022-01-25)
^^^^^^^^^^^^^^^^^^^
- Add compatibility with Django 4.0
- Fix regression where settings receiving a default were ignored. #323 #327
v2.3.1 (2021-11-08)
^^^^^^^^^^^^^^^^^^^
- Test Django 3.2 on Python 3.10 as well.
- Test on PyPy 3.6, 3.7 and 3.8.
- Enforce Python version requirement during installation (>=3.6).
- Fix and refactor the documentation build process.
v2.3 (2021-10-27)
^^^^^^^^^^^^^^^^^
- **BACKWARD INCOMPATIBLE** Drop support for Python 2.7 and 3.5.
- **BACKWARD INCOMPATIBLE** Drop support for Django < 2.2.
- Add support for Django 3.1 and 3.2.
- Add suppport for Python 3.9 and 3.10.
- Deprecate ``utils.import_by_path`` in favor of
``django.utils.module_loading.import_string``.
- Add ASGI support.
- Added "python -m configurations" entry point.
- Make package ``install_requires`` include ``django>=2.2``.
- Prevent an ImproperlyConfigured warning from ``DEFAULT_HASHING_ALGORITHM``.
- Prevent warnings for settings deprecated in Django 2.2
(``DEFAULT_CONTENT_TYPE`` and ``FILE_CHARSET``).
- Preserve Django warnings when ``DEFAULT_AUTO_FIELD`` is not set.
- Miscellaneous documentation fixes.
- Miscellaneous internal improvements.
v2.2 (2019-12-03)
^^^^^^^^^^^^^^^^^
- **BACKWARD INCOMPATIBLE** Drop support for Python 3.4.
- **BACKWARD INCOMPATIBLE** Drop support for Django < 1.11.
- Add support for Django 3.0.
- Add support for Python 3.8.
- Add support for PyPy 3.
- Replace ``django.utils.six`` with ``six`` to support Django >= 3.
- Start using tox-travis and setuptools-scm for simplified test harness
and release management.
v2.1 (2018-08-16) v2.1 (2018-08-16)
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^

View file

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

View file

@ -8,8 +8,8 @@ Calling a Django management command
If you want to call a Django management command programmatically, say If you want to call a Django management command programmatically, say
from a script outside of your usual Django code, you can use the from a script outside of your usual Django code, you can use the
equivalent of Django's :func:`~django.core.management.call_command` equivalent of Django's :func:`~django.core.management.call_command` function
function with django-configurations, too. with django-configurations, too.
Simply import it from ``configurations.management`` instead: Simply import it from ``configurations.management`` instead:
@ -20,43 +20,6 @@ Simply import it from ``configurations.management`` instead:
call_command('dumpdata', exclude=['contenttypes', 'auth']) call_command('dumpdata', exclude=['contenttypes', 'auth'])
Read .env file
--------------
Configurations can read values for environment variables out of an ``.env``
file, and push them into the application's process environment. Simply set
the ``DOTENV`` setting to the appropriate file name:
.. code-block:: python
# mysite/settings.py
import os.path
from configurations import Configuration, values
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
class Dev(Configuration):
DOTENV = os.path.join(BASE_DIR, '.env')
SECRET_KEY = values.SecretValue()
API_KEY1 = values.Value()
API_KEY2 = values.Value()
API_KEY3 = values.Value('91011')
A ``.env`` file is a ``.ini``-style file. It must contain a list of
``KEY=value`` pairs, just like Shell environment variables:
.. code-block:: ini
# .env
DJANGO_DEBUG=False
DJANGO_SECRET_KEY=1q2w3e4r5t6z7u8i9o0(%&)$§!pqaycz
API_KEY1=1234
API_KEY2=5678
Envdir Envdir
------ ------
@ -74,7 +37,7 @@ Example:
.. code-block:: console .. code-block:: console
$ tree --noreport mysite_env/ $ tree mysite_env/
mysite_env/ mysite_env/
├── DJANGO_SETTINGS_MODULE ├── DJANGO_SETTINGS_MODULE
├── DJANGO_DEBUG ├── DJANGO_DEBUG
@ -82,8 +45,10 @@ Example:
├── DJANGO_CACHE_URL ├── DJANGO_CACHE_URL
└── PYTHONSTARTUP └── PYTHONSTARTUP
0 directories, 3 files
$ cat mysite_env/DJANGO_CACHE_URL $ cat mysite_env/DJANGO_CACHE_URL
redis://user@host:port/1 redis://user@host:port/1
$
Then, to enable the ``mysite_env`` environment variables, simply use the Then, to enable the ``mysite_env`` environment variables, simply use the
``envdir`` command line tool as a prefix for your program, e.g.: ``envdir`` command line tool as a prefix for your program, e.g.:
@ -97,42 +62,6 @@ Python instead of from the command line.
.. _envdir: https://pypi.python.org/pypi/envdir .. _envdir: https://pypi.python.org/pypi/envdir
Sentry (dynamic setup calls)
----------------------------
For all tools that require an initialization call you should use
:ref:`Setup methods<setup-methods>` (unless you want them activated
for all environments).
Intuitively you might want to add the required setup call like any
other setting:
.. code-block:: python
class Prod(Base):
# ...
sentry_sdk.init("your dsn", integrations=[DjangoIntegration()])
But this will activate, in this case, Sentry even when you're running a
Dev configuration. What you should do instead, is put that code in the
``post_setup`` function. That way Sentry will only ever run when Prod
is the selected configuration:
.. code-block:: python
class Prod(Base):
# ...
@classmethod
def post_setup(cls):
"""Sentry initialization"""
super(Prod, cls).post_setup()
sentry_sdk.init(
dsn=os.environ.get("your dsn"), integrations=[DjangoIntegration()]
)
.. _project-templates: .. _project-templates:
Project templates Project templates
@ -149,13 +78,13 @@ First install Django 1.8.x and django-configurations:
.. code-block:: console .. code-block:: console
$ python -m pip install -r https://raw.github.com/jazzband/django-configurations/templates/1.8.x/requirements.txt $ pip install -r https://raw.github.com/jazzband/django-configurations/templates/1.8.x/requirements.txt
Or Django 1.8: Or Django 1.8:
.. code-block:: console .. code-block:: console
$ python -m django startproject mysite -v2 --template https://github.com/jazzband/django-configurations/archive/templates/1.8.x.zip $ django-admin.py startproject mysite -v2 --template https://github.com/jazzband/django-configurations/archive/templates/1.8.x.zip
Now you have a default Django 1.8.x project in the ``mysite`` Now you have a default Django 1.8.x project in the ``mysite``
directory that uses django-configurations. directory that uses django-configurations.
@ -180,7 +109,7 @@ probably just add the following to the **beginning** of your settings module:
import configurations import configurations
configurations.setup() configurations.setup()
That has the same effect as using the ``manage.py``, ``wsgi.py`` or ``asgi.py`` utilities. That has the same effect as using the ``manage.py`` or ``wsgi.py`` utilities.
This will also call ``django.setup()``. This will also call ``django.setup()``.
>= 3.1 >= 3.1
@ -265,7 +194,6 @@ It also works with django-extensions's shell_plus_ management command.
.. _`manage your IPython profile`: http://ipython.org/ipython-doc/dev/config/overview.html#configuration-file-location .. _`manage your IPython profile`: http://ipython.org/ipython-doc/dev/config/overview.html#configuration-file-location
.. _shell_plus: https://django-extensions.readthedocs.io/en/latest/shell_plus.html .. _shell_plus: https://django-extensions.readthedocs.io/en/latest/shell_plus.html
FastCGI FastCGI
------- -------
@ -330,8 +258,8 @@ Channels
-------- --------
If you want to deploy a project that uses the Django channels with If you want to deploy a project that uses the Django channels with
`Daphne <http://github.com/django/daphne/>`_ as the `Daphne <http://github.com/django/daphne/>` as the
`interface server <http://channels.readthedocs.io/en/latest/deploying.html#run-interface-servers>`_ `interface server <http://channels.readthedocs.io/en/latest/deploying.html#run-interface-servers>`
you have to use a asgi.py script similar to the following: you have to use a asgi.py script similar to the following:
.. code-block:: python .. code-block:: python

View file

@ -93,6 +93,6 @@ Bugs and feature requests
As always your mileage may vary, so please don't hesitate to send feature As always your mileage may vary, so please don't hesitate to send feature
requests and bug reports: requests and bug reports:
- https://github.com/jazzband/django-configurations/issues https://github.com/jazzband/django-configurations/issues
Thanks! Thanks!

190
docs/make.bat Normal file
View file

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

View file

@ -3,16 +3,14 @@ Usage patterns
There are various configuration patterns that can be implemented with There are various configuration patterns that can be implemented with
django-configurations. The most common pattern is to have a base class django-configurations. The most common pattern is to have a base class
and various subclasses based on the environment they are supposed to be and various subclasses based on the enviroment they are supposed to be
used in, e.g. in production, staging and development. used in, e.g. in production, staging and development.
Server specific settings Server specific settings
------------------------ ------------------------
For example, imagine you have a base setting class in your **settings.py** For example, imagine you have a base setting class in your **settings.py**
file: file::
.. code-block:: python
from configurations import Configuration from configurations import Configuration
@ -21,54 +19,47 @@ file:
class Dev(Base): class Dev(Base):
DEBUG = True DEBUG = True
TEMPLATE_DEBUG = DEBUG
class Prod(Base): class Prod(Base):
TIME_ZONE = 'America/New_York' TIME_ZONE = 'America/New_York'
You can now set the ``DJANGO_CONFIGURATION`` environment variable to You can now set the ``DJANGO_CONFIGURATION`` environment variable to one
one of the class names you've defined, e.g. on your production server of the class names you've defined, e.g. on your production server it
it should be ``Prod``. In Bash that would be: should be ``Prod``. In bash that would be::
.. code-block:: console export DJANGO_SETTINGS_MODULE=mysite.settings
export DJANGO_CONFIGURATION=Prod
$ export DJANGO_SETTINGS_MODULE=mysite.settings python manage.py runserver
$ export DJANGO_CONFIGURATION=Prod
$ python -m manage runserver
Alternatively you can use the ``--configuration`` option when using Django Alternatively you can use the ``--configuration`` option when using Django
management commands along the lines of Django's default ``--settings`` management commands along the lines of Django's default ``--settings``
command line option, e.g. command line option, e.g.::
.. code-block:: console python manage.py runserver --settings=mysite.settings --configuration=Prod
$ python -m manage runserver --settings=mysite.settings --configuration=Prod
Property settings Property settings
----------------- -----------------
Use a ``property`` to allow for computed settings. This pattern can Use a `property` to allow for computed settings. This pattern can also be used to postpone / lazy evaluate a value. E.g. useful when nesting a Value in a dictionary and a string is required::
also be used to postpone / lazy evaluate a value. E.g., useful when
nesting a Value in a dictionary and a string is required:
.. code-block:: python
class Prod(Configuration): class Prod(Configuration):
SOME_VALUE = values.Value(None, environ_prefix=None) SENTRY_DSN = values.Value(None, environ_prefix=None)
@property @property
def SOME_CONFIG(self): def RAVEN_CONFIG(self):
return { return {
'some_key': self.SOME_VALUE, 'dsn': self.SENTRY_DSN,
} }
Global settings defaults Global settings defaults
------------------------ ------------------------
Every ``configurations.Configuration`` subclass will automatically Every ``configurations.Configuration`` subclass will automatically contain
contain Django's global settings as class attributes, so you can refer Django's global settings as class attributes, so you can refer to them when
to them when setting other values, e.g. setting other values, e.g.::
.. code-block:: python
from configurations import Configuration from configurations import Configuration
@ -86,17 +77,13 @@ Configuration mixins
You might want to apply some configuration values for each and every You might want to apply some configuration values for each and every
project you're working on without having to repeat yourself. Just define project you're working on without having to repeat yourself. Just define
a few mixin you re-use multiple times: a few mixin you re-use multiple times::
.. code-block:: python class FullPageCaching(object):
class FullPageCaching:
USE_ETAGS = True USE_ETAGS = True
Then import that mixin class in your site settings module and use it with Then import that mixin class in your site settings module and use it with
a ``Configuration`` class: a ``Configuration`` class::
.. code-block:: python
from configurations import Configuration from configurations import Configuration
@ -110,10 +97,8 @@ Pristine methods
.. versionadded:: 0.3 .. versionadded:: 0.3
In case one of your settings itself need to be a callable, you need to In case one of your settings itself need to be a callable, you need to
tell that django-configurations by using the ``pristinemethod`` tell that django-configurations by using the ``pristinemethod`` decorator,
decorator, e.g. e.g.::
.. code-block:: python
from configurations import Configuration, pristinemethod from configurations import Configuration, pristinemethod
@ -123,18 +108,13 @@ decorator, e.g.
def ACCESS_FUNCTION(user): def ACCESS_FUNCTION(user):
return user.is_staff return user.is_staff
Lambdas work, too: Lambdas work, too::
.. code-block:: python
from configurations import Configuration, pristinemethod from configurations import Configuration, pristinemethod
class Prod(Configuration): class Prod(Configuration):
ACCESS_FUNCTION = pristinemethod(lambda user: user.is_staff) ACCESS_FUNCTION = pristinemethod(lambda user: user.is_staff)
.. _setup-methods:
Setup methods Setup methods
------------- -------------
@ -143,9 +123,7 @@ Setup methods
If there is something required to be set up before, during or after the If there is something required to be set up before, during or after the
settings loading happens, please override the ``pre_setup``, ``setup`` or settings loading happens, please override the ``pre_setup``, ``setup`` or
``post_setup`` class methods like so (don't forget to apply the Python ``post_setup`` class methods like so (don't forget to apply the Python
``@classmethod`` decorator): ``@classmethod`` decorator)::
.. code-block:: python
import logging import logging
from configurations import Configuration from configurations import Configuration
@ -176,9 +154,7 @@ Of course that won't work for ``post_setup`` since that's when the
settings setup is already done. settings setup is already done.
In fact you can easily do something unrelated to settings, like In fact you can easily do something unrelated to settings, like
connecting to a database: connecting to a database::
.. code-block:: python
from configurations import Configuration from configurations import Configuration
@ -190,12 +166,13 @@ connecting to a database:
import mango import mango
mango.connect('enterprise') mango.connect('enterprise')
.. warning:: .. warning::
You could do the same by overriding the ``__init__`` method of your You could do the same by overriding the ``__init__`` method of your
settings class but this may cause hard to debug errors because settings class but this may cause hard to debug errors because
at the time the ``__init__`` method is called (during Django at the time the ``__init__`` method is called (during Django startup)
startup) the Django setting system isn't fully loaded yet. the Django setting system isn't fully loaded yet.
So anything you do in ``__init__`` that may require So anything you do in ``__init__`` that may require
``django.conf.settings`` or Django models there is a good chance it ``django.conf.settings`` or Django models there is a good chance it
@ -204,16 +181,15 @@ connecting to a database:
.. versionchanged:: 0.4 .. versionchanged:: 0.4
A new ``setup`` method was added to be able to handle the new A new ``setup`` method was added to be able to handle the new
:class:`~configurations.values.Value` classes and allow an :class:`~configurations.values.Value` classes and allow an in-between
in-between modification of the configuration values. modification of the configuration values.
Standalone scripts Standalone scripts
------------------ ------------------
If you want to run scripts outside of your project you need to add If you want to run scripts outside of your project you need to add these lines
these lines on top of your file: on top of your file::
.. code-block:: python
import configurations import configurations
configurations.setup() configurations.setup()

View file

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

View file

@ -46,7 +46,7 @@ value:
class Dev(Configuration): class Dev(Configuration):
DEBUG = values.BooleanValue(True) DEBUG = values.BooleanValue(True)
DEBUG_PROPAGATE_EXCEPTIONS = values.BooleanValue(DEBUG) TEMPLATE_DEBUG = values.BooleanValue(DEBUG)
See the list of :ref:`built-in value classes<built-ins>` for more information. See the list of :ref:`built-in value classes<built-ins>` for more information.
@ -86,11 +86,9 @@ prefixed with ``DJANGO_``. E.g.:
django-configurations will try to read the ``DJANGO_ROOT_URLCONF`` environment django-configurations will try to read the ``DJANGO_ROOT_URLCONF`` environment
variable when deciding which value the ``ROOT_URLCONF`` setting should have. variable when deciding which value the ``ROOT_URLCONF`` setting should have.
When you run the web server simply specify that environment variable When you run the web server simply specify that environment variable
(e.g. in your init script): (e.g. in your init script)::
.. code-block:: console DJANGO_ROOT_URLCONF=mysite.debugging_urls gunicorn mysite.wsgi:application
$ DJANGO_ROOT_URLCONF=mysite.debugging_urls gunicorn mysite.wsgi:application
If the environment variable can't be found it'll use the default If the environment variable can't be found it'll use the default
``'mysite.urls'``. ``'mysite.urls'``.
@ -127,9 +125,7 @@ Allow final value to be used outside the configuration context
You may use the ``environ_name`` parameter to allow a :class:`~Value` to be You may use the ``environ_name`` parameter to allow a :class:`~Value` to be
directly converted to its final value for use outside of the configuration directly converted to its final value for use outside of the configuration
context: context::
.. code-block:: pycon
>>> type(values.Value([])) >>> type(values.Value([]))
<class 'configurations.values.Value'> <class 'configurations.values.Value'>
@ -164,12 +160,12 @@ the prefix.
:param default: the default value of the setting :param default: the default value of the setting
:param environ: toggle for environment use :param environ: toggle for environment use
:param environ_name: capitalized name of environment variable to look for :param environ_name: name of environment variable to look for
:param environ_prefix: capitalized prefix to use when looking for environment variable :param environ_prefix: prefix to use when looking for environment variable
:param environ_required: whether or not the value is required to be set as an environment variable :param environ_required: wheter or not the value is required to be set as an environment variable
:type environ: bool :type environ: bool
:type environ_name: str or None :type environ_name: capitalized string or None
:type environ_prefix: str :type environ_prefix: capitalized string
:type environ_required: bool :type environ_required: bool
The ``default`` parameter is effectively the value the setting has The ``default`` parameter is effectively the value the setting has
@ -260,10 +256,6 @@ Type values
MYSITE_CONVERSION_RATE = values.DecimalValue(decimal.Decimal('4.56214')) MYSITE_CONVERSION_RATE = values.DecimalValue(decimal.Decimal('4.56214'))
.. class:: SequenceValue
Common base class for sequence values.
.. class:: ListValue(default, [separator=',', converter=None]) .. class:: ListValue(default, [separator=',', converter=None])
A :class:`~SequenceValue` subclass that handles list values. A :class:`~SequenceValue` subclass that handles list values.
@ -287,21 +279,17 @@ Type values
MONTY_PYTHONS = ListValue(['John Cleese', 'Eric Idle'], MONTY_PYTHONS = ListValue(['John Cleese', 'Eric Idle'],
converter=check_monty_python) converter=check_monty_python)
You can override this list with an environment variable like this: You can override this list with an environment variable like this::
.. code-block:: console DJANGO_MONTY_PYTHONS="Terry Jones,Graham Chapman" gunicorn mysite.wsgi:application
$ DJANGO_MONTY_PYTHONS="Terry Jones,Graham Chapman" gunicorn mysite.wsgi:application
Use a custom separator:: Use a custom separator::
EMERGENCY_EMAILS = ListValue(['admin@mysite.net'], separator=';') EMERGENCY_EMAILS = ListValue(['admin@mysite.net'], separator=';')
And override it: And override it::
.. code-block:: console DJANGO_EMERGENCY_EMAILS="admin@mysite.net;manager@mysite.org;support@mysite.com" gunicorn mysite.wsgi:application
$ DJANGO_EMERGENCY_EMAILS="admin@mysite.net;manager@mysite.org;support@mysite.com" gunicorn mysite.wsgi:application
.. class:: TupleValue .. class:: TupleValue
@ -313,10 +301,6 @@ Type values
See the :class:`~ListValue` examples above. See the :class:`~ListValue` examples above.
.. class:: SingleNestedSequenceValue
Common base class for nested sequence values.
.. class:: SingleNestedTupleValue(default, [seq_separator=';', separator=',', converter=None]) .. class:: SingleNestedTupleValue(default, [seq_separator=';', separator=',', converter=None])
A :class:`~SingleNestedSequenceValue` subclass that handles single nested tuple values, A :class:`~SingleNestedSequenceValue` subclass that handles single nested tuple values,
@ -370,10 +354,6 @@ Type values
'it': ['Mike', 'Joe'], 'it': ['Mike', 'Joe'],
}) })
Override using environment variables like this::
DJANGO_DEPARTMENTS={'it':['Mike','Joe'],'hr':['Emma','Olivia']}
Validator values Validator values
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
@ -567,7 +547,7 @@ Other values
:: ::
MIDDLEWARE = values.BackendsValue([ MIDDLEWARE_CLASSES = values.BackendsValue([
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@ -581,8 +561,7 @@ Other values
A :class:`~Value` subclass that doesn't allow setting a default value A :class:`~Value` subclass that doesn't allow setting a default value
during instantiation and force-enables the use of an environment variable during instantiation and force-enables the use of an environment variable
to reduce the risk of accidentally storing secret values in the settings to reduce the risk of accidentally storing secret values in the settings
file. This usually resolves to ``DJANGO_SECRET_KEY`` unless you have file.
customized the environment variable names.
:raises: ``ValueError`` when given a default value :raises: ``ValueError`` when given a default value
@ -604,7 +583,7 @@ Value mixins
requires a ``caster`` class attribute of one of the following types: requires a ``caster`` class attribute of one of the following types:
- dotted import path, e.g. ``'mysite.utils.custom_caster'`` - dotted import path, e.g. ``'mysite.utils.custom_caster'``
- a callable, e.g. :class:`int` - a callable, e.g. :func:`int`
Example:: Example::
@ -625,7 +604,7 @@ Value mixins
validation attempt. validation attempt.
- dotted import path, e.g. ``'mysite.validators.custom_validator'`` - dotted import path, e.g. ``'mysite.validators.custom_validator'``
- a callable, e.g. :class:`bool` - a callable, e.g. :func:`bool`
Example:: Example::

View file

@ -1,10 +1,9 @@
[wheel]
universal = 1
[coverage:run] [coverage:run]
source = . source = .
include = configurations/*,tests/*
branch = 1 branch = 1
parallel = 1
[coverage:report] [coverage:report]
include = configurations/*,tests/* include = configurations/*,tests/*
[flake8]
exclude = .tox,docs/*,.eggs
ignore = E501,E127,E128,E124,W503

View file

@ -1,26 +1,38 @@
from __future__ import print_function
import ast
import os import os
import codecs import codecs
from setuptools import setup from setuptools import setup
class VersionFinder(ast.NodeVisitor):
def __init__(self):
self.version = None
def visit_Assign(self, node):
if node.targets[0].id == '__version__':
self.version = node.value.s
def read(*parts): def read(*parts):
filename = os.path.join(os.path.dirname(__file__), *parts) filename = os.path.join(os.path.dirname(__file__), *parts)
with codecs.open(filename, encoding='utf-8') as fp: with codecs.open(filename, encoding='utf-8') as fp:
return fp.read() return fp.read()
def find_version(*parts):
finder = VersionFinder()
finder.visit(ast.parse(read(*parts)))
return finder.version
setup( setup(
name="django-configurations", name="django-configurations",
use_scm_version={"version_scheme": "post-release", "local_scheme": "dirty-tag"}, version=find_version("configurations", "__init__.py"),
setup_requires=["setuptools_scm"],
url='https://django-configurations.readthedocs.io/', url='https://django-configurations.readthedocs.io/',
project_urls={
'Source': 'https://github.com/jazzband/django-configurations',
},
license='BSD', license='BSD',
description="A helper for organizing Django settings.", description="A helper for organizing Django settings.",
long_description=read('README.rst'), long_description=read('README.rst'),
long_description_content_type='text/x-rst',
author='Jannis Leidel', author='Jannis Leidel',
author_email='jannis@leidel.info', author_email='jannis@leidel.info',
packages=['configurations'], packages=['configurations'],
@ -29,42 +41,18 @@ setup(
'django-cadmin = configurations.management:execute_from_command_line', 'django-cadmin = configurations.management:execute_from_command_line',
], ],
}, },
install_requires=[
'django>=3.2',
],
python_requires='>=3.9, <4.0',
extras_require={
'cache': ['django-cache-url'],
'database': ['dj-database-url'],
'email': ['dj-email-url'],
'search': ['dj-search-url'],
'testing': [
'django-cache-url>=1.0.0',
'dj-database-url',
'dj-email-url',
'dj-search-url',
],
},
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Framework :: Django', 'Framework :: Django',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Framework :: Django :: 5.1',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Utilities', 'Topic :: Utilities',
], ],
zip_safe=False, zip_safe=False,

View file

@ -5,6 +5,7 @@ class Base(Configuration):
# Django settings for test_project project. # Django settings for test_project project.
DEBUG = values.BooleanValue(True, environ=True) DEBUG = values.BooleanValue(True, environ=True)
TEMPLATE_DEBUG = DEBUG
ADMINS = ( ADMINS = (
# ('Your Name', 'your_email@example.com'), # ('Your Name', 'your_email@example.com'),
@ -83,6 +84,7 @@ class Base(Configuration):
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
) )
# Make this unique, and don't share it with anybody. # Make this unique, and don't share it with anybody.
@ -94,7 +96,7 @@ class Base(Configuration):
'django.template.loaders.app_directories.Loader', 'django.template.loaders.app_directories.Loader',
) )
MIDDLEWARE = ( MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',

View file

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

View file

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

30
tests/docs/conf.py Normal file
View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
# setup Django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.main")
os.environ.setdefault('DJANGO_CONFIGURATION', 'Test')
extensions = [
'configurations.sphinx',
]
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'django-configurations'
copyright = '2012-2014, Jannis Leidel and other contributors'
version = release = 'test'
exclude_patterns = ['_build']
html_theme = 'default'

2
tests/docs/index.rst Normal file
View file

@ -0,0 +1,2 @@
Test Documentation
^^^^^^^^^^^^^^^^^^

View file

View file

View file

@ -0,0 +1,16 @@
from optparse import make_option
from django.core.management.base import BaseCommand
class Command(BaseCommand):
# Used by a specific test to see how unupgraded
# management commands play with configurations.
# See the test code for more details.
option_list = BaseCommand.option_list + (
make_option('--arg1', action='store_true'),
)
def handle(self, *args, **options):
pass

9
tests/requirements.txt Normal file
View file

@ -0,0 +1,9 @@
coverage
django-discover-runner
mock
dj-database-url
dj-email-url
dj-search-url
django-cache-url>=1.0.0
six
Sphinx>=1.4

View file

@ -6,6 +6,3 @@ class DotEnvConfiguration(Configuration):
DOTENV = 'test_project/.env' DOTENV = 'test_project/.env'
DOTENV_VALUE = values.Value() DOTENV_VALUE = values.Value()
def DOTENV_VALUE_METHOD(self):
return values.Value(environ_name="DOTENV_VALUE")

View file

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

View file

@ -1,7 +1,9 @@
import os import os
import uuid import uuid
import django
from configurations import Configuration, pristinemethod from configurations import Configuration, pristinemethod
from configurations.values import BooleanValue
class Test(Configuration): class Test(Configuration):
@ -9,6 +11,8 @@ class Test(Configuration):
os.path.join(os.path.dirname( os.path.join(os.path.dirname(
os.path.abspath(__file__)), os.pardir)) os.path.abspath(__file__)), os.pardir))
ENV_LOADED = BooleanValue(False)
DEBUG = True DEBUG = True
SITE_ID = 1 SITE_ID = 1
@ -27,14 +31,18 @@ class Test(Configuration):
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.admin',
'tests', 'tests',
] ]
ROOT_URLCONF = 'tests.urls' ROOT_URLCONF = 'tests.urls'
if django.VERSION[:2] < (1, 6):
TEST_RUNNER = 'discover_runner.DiscoverRunner'
@property @property
def ALLOWED_HOSTS(self): def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:] allowed_hosts = super(Test, self).ALLOWED_HOSTS[:]
allowed_hosts.append('base') allowed_hosts.append('base')
return allowed_hosts return allowed_hosts
@ -64,7 +72,3 @@ class Test(Configuration):
@classmethod @classmethod
def post_setup(cls): def post_setup(cls):
cls.POST_SETUP_TEST_SETTING = 7 cls.POST_SETUP_TEST_SETTING = 7
class TestWithDefaultSetExplicitely(Test):
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View file

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

View file

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

View file

@ -5,6 +5,6 @@ class Inheritance(Base):
@property @property
def ALLOWED_HOSTS(self): def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:] allowed_hosts = super(Inheritance, self).ALLOWED_HOSTS[:]
allowed_hosts.append('test') allowed_hosts.append('test')
return allowed_hosts return allowed_hosts

View file

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

View file

@ -1,6 +1,6 @@
import os import os
from django.test import TestCase from django.test import TestCase
from unittest.mock import patch from mock import patch
class DotEnvLoadingTests(TestCase): class DotEnvLoadingTests(TestCase):
@ -11,5 +11,4 @@ class DotEnvLoadingTests(TestCase):
def test_env_loaded(self): def test_env_loaded(self):
from tests.settings import dot_env from tests.settings import dot_env
self.assertEqual(dot_env.DOTENV_VALUE, 'is set') self.assertEqual(dot_env.DOTENV_VALUE, 'is set')
self.assertEqual(dot_env.DOTENV_VALUE_METHOD, 'is set')
self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV) self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV)

View file

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

View file

@ -2,7 +2,7 @@ import os
from django.test import TestCase from django.test import TestCase
from unittest.mock import patch from mock import patch
class InheritanceTests(TestCase): class InheritanceTests(TestCase):

View file

@ -2,12 +2,15 @@ import os
import subprocess import subprocess
import sys import sys
from django import VERSION as DJANGO_VERSION
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from unittest.mock import patch from unittest import skipIf
from configurations.importer import ConfigurationFinder from mock import patch
from configurations.importer import ConfigurationImporter
ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
TEST_PROJECT_DIR = os.path.join(ROOT_DIR, 'test_project') TEST_PROJECT_DIR = os.path.join(ROOT_DIR, 'test_project')
@ -42,14 +45,12 @@ class MainTests(TestCase):
@patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test') @patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test')
def test_empty_module_var(self): def test_empty_module_var(self):
with self.assertRaises(ImproperlyConfigured): self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
ConfigurationFinder()
@patch.dict(os.environ, clear=True, @patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main') DJANGO_SETTINGS_MODULE='tests.settings.main')
def test_empty_class_var(self): def test_empty_class_var(self):
with self.assertRaises(ImproperlyConfigured): self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
ConfigurationFinder()
def test_global_settings(self): def test_global_settings(self):
from configurations.base import Configuration from configurations.base import Configuration
@ -57,12 +58,6 @@ class MainTests(TestCase):
self.assertEqual(repr(Configuration), self.assertEqual(repr(Configuration),
"<Configuration 'configurations.base.Configuration'>") "<Configuration 'configurations.base.Configuration'>")
def test_deprecated_settings_but_set_by_user(self):
from tests.settings.main import TestWithDefaultSetExplicitely
TestWithDefaultSetExplicitely.setup()
self.assertEqual(TestWithDefaultSetExplicitely.DEFAULT_AUTO_FIELD,
"django.db.models.BigAutoField")
def test_repr(self): def test_repr(self):
from tests.settings.main import Test from tests.settings.main import Test
self.assertEqual(repr(Test), self.assertEqual(repr(Test),
@ -72,21 +67,21 @@ class MainTests(TestCase):
DJANGO_SETTINGS_MODULE='tests.settings.main', DJANGO_SETTINGS_MODULE='tests.settings.main',
DJANGO_CONFIGURATION='Test') DJANGO_CONFIGURATION='Test')
def test_initialization(self): def test_initialization(self):
finder = ConfigurationFinder() importer = ConfigurationImporter()
self.assertEqual(finder.module, 'tests.settings.main') self.assertEqual(importer.module, 'tests.settings.main')
self.assertEqual(finder.name, 'Test') self.assertEqual(importer.name, 'Test')
self.assertEqual( self.assertEqual(
repr(finder), repr(importer),
"<ConfigurationFinder for 'tests.settings.main.Test'>") "<ConfigurationImporter for 'tests.settings.main.Test'>")
@patch.dict(os.environ, clear=True, @patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.inheritance', DJANGO_SETTINGS_MODULE='tests.settings.inheritance',
DJANGO_CONFIGURATION='Inheritance') DJANGO_CONFIGURATION='Inheritance')
def test_initialization_inheritance(self): def test_initialization_inheritance(self):
finder = ConfigurationFinder() importer = ConfigurationImporter()
self.assertEqual(finder.module, self.assertEqual(importer.module,
'tests.settings.inheritance') 'tests.settings.inheritance')
self.assertEqual(finder.name, 'Inheritance') self.assertEqual(importer.name, 'Inheritance')
@patch.dict(os.environ, clear=True, @patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main', DJANGO_SETTINGS_MODULE='tests.settings.main',
@ -95,12 +90,12 @@ class MainTests(TestCase):
'--settings=tests.settings.main', '--settings=tests.settings.main',
'--configuration=Test']) '--configuration=Test'])
def test_configuration_option(self): def test_configuration_option(self):
finder = ConfigurationFinder(check_options=False) importer = ConfigurationImporter(check_options=False)
self.assertEqual(finder.module, 'tests.settings.main') self.assertEqual(importer.module, 'tests.settings.main')
self.assertEqual(finder.name, 'NonExisting') self.assertEqual(importer.name, 'NonExisting')
finder = ConfigurationFinder(check_options=True) importer = ConfigurationImporter(check_options=True)
self.assertEqual(finder.module, 'tests.settings.main') self.assertEqual(importer.module, 'tests.settings.main')
self.assertEqual(finder.name, 'Test') self.assertEqual(importer.name, 'Test')
def test_configuration_argument_in_cli(self): def test_configuration_argument_in_cli(self):
""" """
@ -114,45 +109,20 @@ class MainTests(TestCase):
stdout=subprocess.PIPE) stdout=subprocess.PIPE)
self.assertIn('--configuration', proc.communicate()[0].decode('utf-8')) self.assertIn('--configuration', proc.communicate()[0].decode('utf-8'))
def test_configuration_argument_in_runypy_cli(self): @skipIf(DJANGO_VERSION >= (1, 10), 'only applies to Django < 1.10')
def test_deprecated_option_list_command(self):
""" """
Verify that's configuration option has been added to managements Verify that the configuration option is correctly added to any
commands when using the -m entry point management commands which are still relying on option_list to
""" add their own custom arguments
proc = subprocess.Popen(
[sys.executable, '-m', 'configurations', 'test', '--help'],
stdout=subprocess.PIPE,
)
self.assertIn('--configuration', proc.communicate()[0].decode('utf-8'))
proc = subprocess.Popen(
[sys.executable, '-m', 'configurations', 'runserver', '--help'],
stdout=subprocess.PIPE,
)
self.assertIn('--configuration', proc.communicate()[0].decode('utf-8'))
def test_django_setup_only_called_once(self): Specific test for a pattern which was deprecated in Django 1.8
and which will become completely unsupported in Django 1.10.
https://docs.djangoproject.com/en/1.8/howto/custom-management-commands/#custom-commands-options
"""
proc = subprocess.Popen( proc = subprocess.Popen(
[sys.executable, os.path.join(os.path.dirname(__file__), ['django-cadmin', 'old_optparse_command', '--help'],
'setup_test.py')],
stdout=subprocess.PIPE) stdout=subprocess.PIPE)
res = proc.communicate() output = proc.communicate()[0].decode('utf-8')
stdout = res[0].decode('utf-8') self.assertIn('--configuration', output)
self.assertIn('--arg1', output)
self.assertIn('setup_1', stdout)
self.assertIn('setup_2', stdout)
self.assertIn('setup_done', stdout)
self.assertEqual(proc.returncode, 0)
def test_utils_reraise(self):
from configurations.utils import reraise
class CustomException(Exception):
pass
with self.assertRaises(CustomException) as cm:
try:
raise CustomException
except Exception as exc:
reraise(exc, "Couldn't setup configuration", None)
self.assertEqual(cm.exception.args, ("Couldn't setup configuration: ",))

21
tests/test_sphinx.py Normal file
View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
import subprocess
import os
from django.test import TestCase
from django.conf import settings
class SphinxTests(TestCase):
docs_dir = os.path.join(settings.BASE_DIR, 'docs')
def test_multiprocessing(self):
output = subprocess.check_output([
'sphinx-build',
'-b',
'html',
'-j 2',
'.',
'_build/html',
], cwd=self.docs_dir, stderr=subprocess.STDOUT)
self.assertIn(b'build succeeded.', output)

View file

@ -2,11 +2,10 @@ import decimal
import os import os
from contextlib import contextmanager from contextlib import contextmanager
from django import VERSION as DJANGO_VERSION
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from unittest.mock import patch from mock import patch
from configurations.values import (Value, BooleanValue, IntegerValue, from configurations.values import (Value, BooleanValue, IntegerValue,
FloatValue, DecimalValue, ListValue, FloatValue, DecimalValue, ListValue,
@ -34,7 +33,7 @@ class ValueTests(TestCase):
def test_value_with_default(self): def test_value_with_default(self):
value = Value('default', environ=False) value = Value('default', environ=False)
self.assertEqual(type(value), str) self.assertEqual(type(value), type('default'))
self.assertEqual(value, 'default') self.assertEqual(value, 'default')
self.assertEqual(str(value), 'default') self.assertEqual(str(value), 'default')
@ -44,17 +43,17 @@ class ValueTests(TestCase):
with env(DJANGO_TEST='override'): with env(DJANGO_TEST='override'):
self.assertEqual(value.setup('TEST'), 'default') self.assertEqual(value.setup('TEST'), 'default')
value = Value(environ_name='TEST') value = Value(environ_name='TEST')
self.assertEqual(type(value), str) self.assertEqual(type(value), type('override'))
self.assertEqual(value, 'override') self.assertEqual(value, 'override')
self.assertEqual(str(value), 'override') self.assertEqual(str(value), 'override')
self.assertEqual(f'{value}', 'override') self.assertEqual('{0}'.format(value), 'override')
self.assertEqual('%s' % value, 'override') self.assertEqual('%s' % value, 'override')
value = Value(environ_name='TEST', late_binding=True) value = Value(environ_name='TEST', late_binding=True)
self.assertEqual(type(value), Value) self.assertEqual(type(value), Value)
self.assertEqual(value.value, 'override') self.assertEqual(value.value, 'override')
self.assertEqual(str(value), 'override') self.assertEqual(str(value), 'override')
self.assertEqual(f'{value}', 'override') self.assertEqual('{0}'.format(value), 'override')
self.assertEqual('%s' % value, 'override') self.assertEqual('%s' % value, 'override')
self.assertEqual(repr(value), repr('override')) self.assertEqual(repr(value), repr('override'))
@ -271,9 +270,9 @@ class ValueTests(TestCase):
def test_set_values_default(self): def test_set_values_default(self):
value = SetValue() value = SetValue()
with env(DJANGO_TEST='2,2'): with env(DJANGO_TEST='2,2'):
self.assertEqual(value.setup('TEST'), {'2', '2'}) self.assertEqual(value.setup('TEST'), set(['2', '2']))
with env(DJANGO_TEST='2, 2 ,'): with env(DJANGO_TEST='2, 2 ,'):
self.assertEqual(value.setup('TEST'), {'2', '2'}) self.assertEqual(value.setup('TEST'), set(['2', '2']))
with env(DJANGO_TEST=''): with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), set()) self.assertEqual(value.setup('TEST'), set())
@ -373,23 +372,16 @@ class ValueTests(TestCase):
value = DatabaseURLValue() value = DatabaseURLValue()
self.assertEqual(value.default, {}) self.assertEqual(value.default, {})
with env(DATABASE_URL='sqlite://'): with env(DATABASE_URL='sqlite://'):
settings_value = value.setup('DATABASE_URL') self.assertEqual(value.setup('DATABASE_URL'), {
# Compare the embedded dicts in the "default" entry so that the difference can be seen if 'default': {
# it fails ... DatabaseURLValue(|) uses an external app that can add additional entries
self.assertDictEqual(
{
'CONN_HEALTH_CHECKS': False,
'CONN_MAX_AGE': 0, 'CONN_MAX_AGE': 0,
'DISABLE_SERVER_SIDE_CURSORS': False,
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'HOST': '', 'HOST': '',
'NAME': ':memory:', 'NAME': ':memory:',
'PASSWORD': '', 'PASSWORD': '',
'PORT': '', 'PORT': '',
'USER': '', 'USER': '',
}, }})
settings_value['default']
)
def test_database_url_additional_args(self): def test_database_url_additional_args(self):
@ -419,7 +411,6 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': 'password', 'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com', 'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587, 'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False, 'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True}) 'EMAIL_USE_TLS': True})
with env(EMAIL_URL='console://'): with env(EMAIL_URL='console://'):
@ -430,7 +421,6 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': None, 'EMAIL_HOST_PASSWORD': None,
'EMAIL_HOST_USER': None, 'EMAIL_HOST_USER': None,
'EMAIL_PORT': None, 'EMAIL_PORT': None,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False, 'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': False}) 'EMAIL_USE_TLS': False})
with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:wrong'): # noqa: E501 with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:wrong'): # noqa: E501
@ -439,7 +429,7 @@ class ValueTests(TestCase):
def test_cache_url_value(self): def test_cache_url_value(self):
cache_setting = { cache_setting = {
'default': { 'default': {
'BACKEND': 'django_redis.cache.RedisCache' if DJANGO_VERSION < (4,) else 'django.core.cache.backends.redis.RedisCache', # noqa: E501 'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://host:6379/1', 'LOCATION': 'redis://host:6379/1',
} }
} }
@ -495,12 +485,12 @@ class ValueTests(TestCase):
self.assertEqual(value.value, set()) self.assertEqual(value.value, set())
value = SetValue([1, 2]) value = SetValue([1, 2])
self.assertEqual(value.default, {1, 2}) self.assertEqual(value.default, set([1, 2]))
self.assertEqual(value.value, {1, 2}) self.assertEqual(value.value, set([1, 2]))
def test_setup_value(self): def test_setup_value(self):
class Target: class Target(object):
pass pass
value = EmailURLValue() value = EmailURLValue()
@ -513,7 +503,6 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': 'password', 'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com', 'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587, 'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False, 'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True 'EMAIL_USE_TLS': True
}) })

View file

@ -1,2 +1,6 @@
from django.conf.urls import url
from django.contrib import admin
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls),
] ]

91
tox.ini
View file

@ -1,72 +1,47 @@
[tox] [tox]
skipsdist = true skipsdist = True
usedevelop = true usedevelop = True
minversion = 1.8 minversion = 1.8
whitelist_externals=sphinx-build
envlist = envlist =
py311-checkqa flake8-py27,
docs flake8-py36,
py{39}-dj{32,41,42} readme-py27,
py{310,py310}-dj{32,41,42,50,main} py{27,34,35,36,py}-dj{18,110,111}
py{311}-dj{41,42,50,51,main} py{34,35,36,37}-dj20
py{312}-dj{50,51,main} py{35,36,37}-dj{21,master}
py{313}-dj{50,51,main}
[gh-actions]
python =
3.9: py39
3.10: py310
3.11: py311,flake8,readme
3.12: py312
3.13: py313
pypy-3.10: pypy310
[testenv] [testenv]
usedevelop = true usedevelop = true
setenv = setenv =
DJANGO_SETTINGS_MODULE = tests.settings.main DJANGO_SETTINGS_MODULE = tests.settings.main
DJANGO_CONFIGURATION = Test DJANGO_CONFIGURATION = Test
COVERAGE_PROCESS_START = {toxinidir}/setup.cfg
deps = deps =
dj32: django~=3.2.9 -rtests/requirements.txt
dj41: django~=4.1.3 dj18: django>=1.8,<1.9
dj42: django~=4.2.0 dj110: django>=1.10,<1.11
dj50: django~=5.0.0 dj111: django>=1.11,<2.0
dj51: django~=5.1.0 dj20: django>=2.0a1,<2.1
djmain: https://github.com/django/django/archive/main.tar.gz dj21: django>=2.1a1,<2.2
py312: setuptools djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
py312: wheel
py313: setuptools
py313: wheel
coverage
coverage_enable_subprocess
extras = testing
commands = commands =
python --version python --version
{envbindir}/coverage run {envbindir}/django-cadmin test -v2 {posargs:tests} coverage run {envbindir}/django-cadmin test -v2 {posargs:tests}
coverage combine . tests docs coverage report
coverage report -m --skip-covered
coverage xml
[testenv:py311-checkqa] [testenv:readme-py27]
commands = commands = python setup.py check -r -s
flake8 {toxinidir} deps = readme_renderer
check-manifest -v
python setup.py sdist
twine check dist/*
deps =
flake8
twine
check-manifest
[testenv:docs] [testenv:flake8-py27]
setenv = commands = flake8 configurations tests
deps = deps = flake8
-r docs/requirements.txt
commands = [testenv:flake8-py36]
sphinx-build \ commands = flake8 configurations tests
-b html \ deps = flake8
-a \
-W \ [flake8]
-n \ exclude=.tox
docs \ ignore=E501,E127,E128,E124
docs/_build/html