Merge remote-tracking branch 'origin/master' into vseva/fix_250

This commit is contained in:
Brian Helba 2021-10-25 12:37:36 -04:00
commit 02e8f55ac8
37 changed files with 372 additions and 273 deletions

54
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,54 @@
# something silly
name: Release
on:
push:
tags:
- '*'
jobs:
build:
if: github.repository == 'jazzband/django-configurations'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: release-${{ hashFiles('**/setup.py') }}
restore-keys: |
release-
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U 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@master
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/django-configurations/upload

53
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Test
on:
pull_request:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', 'pypy3']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
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 tox-gh-actions
- name: Tox tests
run: |
tox -v
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
name: Python ${{ matrix.python-version }}
fail_ci_if_error: true

1
.pre-commit-config.yaml Normal file
View file

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

View file

@ -1,37 +0,0 @@
language: python
dist: xenial
cache: pip
python:
- '2.7'
- '3.5'
- '3.6'
- '3.7'
- '3.8'
- 'pypy3'
install: travis_retry pip install tox-travis codecov
script: tox -v
after_success: codecov --required -X gcov fix pycov -f coverage.xml --flags ${TOXENV//-/ }
branches:
except: templates/1.5.x templates/1.6.x
stages:
- test
- name: deploy
if: repo = jazzband/django-configurations AND tag IS present
jobs:
include:
- stage: test
- stage: deploy
install: skip
script: skip
python: 3.7
env: skip
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

46
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,46 @@
# 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,9 +1,10 @@
include README.rst
include CONTRIBUTING.md
include .pre-commit-config.yaml
include AUTHORS
include .travis.yml
include CODE_OF_CONDUCT.md
include CONTRIBUTING.md
include LICENSE
include README.rst
include tox.ini
recursive-include tests *
recursive-include docs *
recursive-include test_project *
include LICENSE
recursive-include tests *

View file

@ -13,9 +13,9 @@ Check out the `documentation`_ for more complete examples.
.. |latest-version| image:: https://img.shields.io/pypi/v/django-configurations.svg
:alt: Latest version on PyPI
:target: https://pypi.python.org/pypi/django-configurations
.. |build-status| image:: https://img.shields.io/travis/jazzband/django-configurations/master.svg
:alt: Build status
:target: https://travis-ci.org/jazzband/django-configurations
.. |build-status| image:: https://github.com/jazzband/django-configurations/workflows/Test/badge.svg
:target: https://github.com/jazzband/django-configurations/actions
:alt: GitHub Actions
.. |codecov| image:: https://codecov.io/github/jazzband/django-configurations/coverage.svg?branch=master
:alt: Codecov
:target: https://codecov.io/github/jazzband/django-configurations?branch=master
@ -81,7 +81,7 @@ command line option, e.g.
python manage.py runserver --settings=mysite.settings --configuration=Dev
To enable Django to use your configuration you now have to modify your
**manage.py** or **wsgi.py** script to use django-configurations's versions
**manage.py**, **wsgi.py** or **asgi.py** script to use django-configurations's versions
of the appropriate starter functions, e.g. a typical **manage.py** using
django-configurations would look like this:
@ -120,5 +120,18 @@ The same applies to your **wsgi.py** file, e.g.:
Here we don't use the default ``django.core.wsgi.get_wsgi_application``
function but instead ``configurations.wsgi.get_wsgi_application``.
Or if you are not serving your app via WSGI but ASGI instead, you need to modify your **asgi.py** file too.:
.. code-block:: python
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'DEV')
from configurations.asgi import get_asgi_application
application = get_asgi_application()
That's it! You can now use your project with ``manage.py`` and your favorite
WSGI enabled server.
WSGI/ASGI enabled server.

8
configurations/asgi.py Normal file
View file

@ -0,0 +1,8 @@
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,5 @@
import os
import re
import six
from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured
@ -33,18 +32,39 @@ class ConfigurationBase(type):
for base in bases[::-1]:
settings_vars.update(uppercase_attributes(base))
attrs = dict(settings_vars, **attrs)
# Fix ImproperlyConfigured issue introduced in Django
deprecated_settings = {
# DEFAULT_HASHING_ALGORITHM is always deprecated, as it's a
# transitional setting
# https://docs.djangoproject.com/en/3.1/releases/3.1/#default-hashing-algorithm-settings
"DEFAULT_HASHING_ALGORITHM",
# DEFAULT_CONTENT_TYPE and FILE_CHARSET are deprecated in
# Django 2.2 and are removed in Django 3.0
"DEFAULT_CONTENT_TYPE",
"FILE_CHARSET",
# When DEFAULT_AUTO_FIELD is not explicitly set, Django's emits a
# system check warning models.W042. This warning should not be
# suppressed, as downstream users are expected to make a decision.
# https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys
"DEFAULT_AUTO_FIELD",
}
# 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_DAYS" in attrs and "PASSWORD_RESET_TIMEOUT" in attrs:
attrs.pop("PASSWORD_RESET_TIMEOUT_DAYS")
return super(ConfigurationBase, cls).__new__(cls, name, bases, attrs)
if "PASSWORD_RESET_TIMEOUT" in attrs:
deprecated_settings.add("PASSWORD_RESET_TIMEOUT_DAYS")
for deprecated_setting in deprecated_settings:
if deprecated_setting in attrs:
del attrs[deprecated_setting]
return super().__new__(cls, name, bases, attrs)
def __repr__(self):
return "<Configuration '{0}.{1}'>".format(self.__module__,
self.__name__)
class Configuration(six.with_metaclass(ConfigurationBase)):
class Configuration(metaclass=ConfigurationBase):
"""
The base configuration class to inherit from.
@ -91,10 +111,10 @@ class Configuration(six.with_metaclass(ConfigurationBase)):
try:
with open(dotenv, 'r') as f:
content = f.read()
except IOError as e:
except OSError as e:
raise ImproperlyConfigured("Couldn't read .env file "
"with the path {}. Error: "
"{}".format(dotenv, e))
"{}".format(dotenv, e)) from e
else:
for line in content.splitlines():
m1 = re.match(r'\A([A-Za-z_0-9]+)=(.*)\Z', line)

View file

@ -51,7 +51,7 @@ def install(check_options=False):
installed = True
class ConfigurationImporter(object):
class ConfigurationImporter:
modvar = SETTINGS_ENVIRONMENT_VARIABLE
namevar = CONFIGURATION_ENVIRONMENT_VARIABLE
error_msg = ("Configuration cannot be imported, "
@ -82,16 +82,10 @@ class ConfigurationImporter(object):
return os.environ.get(self.namevar)
def check_options(self):
try:
parser = base.CommandParser(
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 = base.CommandParser(
usage="%(prog)s subcommand [options] [args]",
add_help=False,
)
parser.add_argument('--settings')
parser.add_argument('--pythonpath')
parser.add_argument(CONFIGURATION_ARGUMENT,
@ -140,7 +134,7 @@ class ConfigurationImporter(object):
return None
class ConfigurationLoader(object):
class ConfigurationLoader:
def __init__(self, name, location):
self.name = name

View file

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

View file

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

View file

@ -1,7 +1,7 @@
from pkg_resources import get_distribution, DistributionNotFound
try:
__version__ = get_distribution(__name__).version
__version__ = get_distribution("django-configurations").version
except DistributionNotFound:
# package is not installed
__version__ = None

View file

@ -2,13 +2,7 @@ from . import importer
importer.install()
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()
from django.core.wsgi import get_wsgi_application # noqa: E402
# this is just for the crazy ones
application = get_wsgi_application()

View file

@ -3,6 +3,20 @@
Changelog
---------
unreleased
^^^^^^^^^^
- **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.
- Deprecate ``utils.import_by_path`` in favor of
``django.utils.module_loading.import_string``.
v2.2 (2019-12-03)
^^^^^^^^^^^^^^^^^

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# django-configurations documentation build configuration file, created by
# sphinx-quickstart on Sat Jul 21 15:03:23 2012.
#
@ -43,8 +41,8 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'django-configurations'
copyright = u'2012-2014, Jannis Leidel and other contributors'
project = 'django-configurations'
copyright = '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
@ -186,8 +184,8 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'django-configurations.tex', u'django-configurations Documentation',
u'Jannis Leidel', 'manual'),
('index', 'django-configurations.tex', 'django-configurations Documentation',
'Jannis Leidel', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@ -216,8 +214,8 @@ latex_documents = [
# 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)
('index', 'django-configurations', 'django-configurations Documentation',
['Jannis Leidel'], 1)
]
# If true, show URL addresses after external links.
@ -230,8 +228,8 @@ man_pages = [
# (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.',
('index', 'django-configurations', 'django-configurations Documentation',
'Jannis Leidel', 'django-configurations', 'One line description of project.',
'Miscellaneous'),
]
@ -248,10 +246,10 @@ texinfo_documents = [
# -- 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'
epub_title = 'django-configurations'
epub_author = 'Jannis Leidel'
epub_publisher = 'Jannis Leidel'
epub_copyright = '2012, Jannis Leidel'
# The language of the text. It defaults to the language option
# or en if the language is not set.
@ -290,7 +288,7 @@ epub_copyright = u'2012, Jannis Leidel'
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'python': ('http://docs.python.org/2.7', None),
'python': ('http://docs.python.org/3', None),
'sphinx': ('http://sphinx.pocoo.org/', None),
'django': ('http://docs.djangoproject.com/en/dev/',
'http://docs.djangoproject.com/en/dev/_objects/'),

View file

@ -182,7 +182,7 @@ probably just add the following to the **beginning** of your settings module:
import configurations
configurations.setup()
That has the same effect as using the ``manage.py`` or ``wsgi.py`` utilities.
That has the same effect as using the ``manage.py``, ``wsgi.py`` or ``asgi.py`` utilities.
This will also call ``django.setup()``.
>= 3.1
@ -332,8 +332,8 @@ Channels
--------
If you want to deploy a project that uses the Django channels with
`Daphne <http://github.com/django/daphne/>` as the
`interface server <http://channels.readthedocs.io/en/latest/deploying.html#run-interface-servers>`
`Daphne <http://github.com/django/daphne/>`_ as the
`interface server <http://channels.readthedocs.io/en/latest/deploying.html#run-interface-servers>`_
you have to use a asgi.py script similar to the following:
.. code-block:: python

View file

@ -21,7 +21,6 @@ file:
class Dev(Base):
DEBUG = True
TEMPLATE_DEBUG = DEBUG
class Prod(Base):
TIME_ZONE = 'America/New_York'
@ -91,7 +90,7 @@ a few mixin you re-use multiple times:
.. code-block:: python
class FullPageCaching(object):
class FullPageCaching:
USE_ETAGS = True
Then import that mixin class in your site settings module and use it with

View file

@ -46,7 +46,6 @@ value:
class Dev(Configuration):
DEBUG = values.BooleanValue(True)
TEMPLATE_DEBUG = values.BooleanValue(DEBUG)
See the list of :ref:`built-in value classes<built-ins>` for more information.
@ -162,7 +161,7 @@ the prefix.
:param environ: toggle for environment use
:param environ_name: name of environment variable to look for
:param environ_prefix: prefix to use when looking for environment variable
:param environ_required: wheter or not the value is required to be set as an environment variable
:param environ_required: whether or not the value is required to be set as an environment variable
:type environ: bool
:type environ_name: capitalized string or None
:type environ_prefix: capitalized string
@ -353,6 +352,10 @@ Type values
DEPARTMENTS = values.DictValue({
'it': ['Mike', 'Joe'],
})
Override using environment variables like this::
DJANGO_DEPARTMENTS={'it':['Mike','Joe'],'hr':['Emma','Olivia']}
Validator values
^^^^^^^^^^^^^^^^
@ -547,7 +550,7 @@ Other values
::
MIDDLEWARE_CLASSES = values.BackendsValue([
MIDDLEWARE = values.BackendsValue([
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',

View file

@ -1,6 +1,3 @@
[wheel]
universal = 1
[coverage:run]
source = .
branch = 1

View file

@ -1,4 +1,3 @@
from __future__ import print_function
import os
import codecs
from setuptools import setup
@ -27,7 +26,10 @@ setup(
'django-cadmin = configurations.management:execute_from_command_line',
],
},
install_requires=['six'],
install_requires=[
'django>=2.2',
'setuptools',
],
extras_require={
'cache': ['django-cache-url'],
'database': ['dj-database-url'],
@ -45,20 +47,18 @@ setup(
classifiers=[
'Development Status :: 5 - Production/Stable',
'Framework :: Django',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0',
'Framework :: Django :: 3.1',
'Framework :: Django :: 3.2',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Utilities',
],

View file

@ -5,7 +5,6 @@ class Base(Configuration):
# Django settings for test_project project.
DEBUG = values.BooleanValue(True, environ=True)
TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@example.com'),
@ -95,7 +94,7 @@ class Base(Configuration):
'django.template.loaders.app_directories.Loader',
)
MIDDLEWARE_CLASSES = (
MIDDLEWARE = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import os
import sys

View file

@ -1,16 +0,0 @@
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

View file

@ -41,7 +41,7 @@ class Test(Configuration):
@property
def ALLOWED_HOSTS(self):
allowed_hosts = super(Test, self).ALLOWED_HOSTS[:]
allowed_hosts = super().ALLOWED_HOSTS[:]
allowed_hosts.append('base')
return allowed_hosts

View file

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

View file

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

View file

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

View file

@ -1,9 +1,6 @@
"""Used by tests to ensure logging is kept when calling setup() twice."""
try:
from unittest import mock
except ImportError:
from mock import mock
from unittest import mock
import configurations

View file

@ -1,6 +1,6 @@
import os
from django.test import TestCase
from mock import patch
from unittest.mock import patch
class DotEnvLoadingTests(TestCase):

View file

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

View file

@ -5,7 +5,7 @@ import sys
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from mock import patch
from unittest.mock import patch
from configurations.importer import ConfigurationImporter

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import subprocess
import os

View file

@ -5,7 +5,7 @@ from contextlib import contextmanager
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from mock import patch
from unittest.mock import patch
from configurations.values import (Value, BooleanValue, IntegerValue,
FloatValue, DecimalValue, ListValue,
@ -270,9 +270,9 @@ class ValueTests(TestCase):
def test_set_values_default(self):
value = SetValue()
with env(DJANGO_TEST='2,2'):
self.assertEqual(value.setup('TEST'), set(['2', '2']))
self.assertEqual(value.setup('TEST'), {'2', '2'})
with env(DJANGO_TEST='2, 2 ,'):
self.assertEqual(value.setup('TEST'), set(['2', '2']))
self.assertEqual(value.setup('TEST'), {'2', '2'})
with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), set())
@ -485,12 +485,12 @@ class ValueTests(TestCase):
self.assertEqual(value.value, set())
value = SetValue([1, 2])
self.assertEqual(value.default, set([1, 2]))
self.assertEqual(value.value, set([1, 2]))
self.assertEqual(value.default, {1, 2})
self.assertEqual(value.value, {1, 2})
def test_setup_value(self):
class Target(object):
class Target:
pass
value = EmailURLValue()

26
tox.ini
View file

@ -4,20 +4,16 @@ usedevelop = true
minversion = 1.8
whitelist_externals = sphinx-build
envlist =
py36-checkqa,
py{27,35,36,py}-dj111
py{35,36,37,py3}-dj20
py{35,36,37,py3}-dj21
py{35,36,37,38,py3}-dj22
py{36,37,38,py3}-dj{30,master}
py36-checkqa
py{36,37,38,39,py3}-dj{22,30,31,32}
py{38,39}-djmain
[travis]
[gh-actions]
python =
2.7: py27
3.5: py35
3.6: py36,flake8,readme
3.7: py37
3.8: py38
3.9: py39
pypy3: pypy3
[testenv]
@ -27,13 +23,11 @@ setenv =
DJANGO_CONFIGURATION = Test
COVERAGE_PROCESS_START = {toxinidir}/setup.cfg
deps =
dj111: django>=1.11,<2.0
dj20: django>=2.0a1,<2.1
dj21: django>=2.1a1,<2.2
dj22: django>=2.2a1,<3.0
dj30: django>=3.0a1,<3.1
djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
py27,pypy: mock
dj22: django~=2.2.17
dj30: django~=3.0.11
dj31: django~=3.1.3
dj32: https://github.com/django/django/archive/stable/3.2.x.tar.gz
djmain: https://github.com/django/django/archive/main.tar.gz
coverage
coverage_enable_subprocess
extras = testing