diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f4ba9c9 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8a8e8d4 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4e6a92c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1 @@ +repos: [] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 150373b..0000000 --- a/.travis.yml +++ /dev/null @@ -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 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e0d5efa --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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/ diff --git a/MANIFEST.in b/MANIFEST.in index 89a0335..3757d2b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 * diff --git a/README.rst b/README.rst index 545ae17..0716754 100644 --- a/README.rst +++ b/README.rst @@ -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. diff --git a/configurations/asgi.py b/configurations/asgi.py new file mode 100644 index 0000000..da9401b --- /dev/null +++ b/configurations/asgi.py @@ -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() diff --git a/configurations/base.py b/configurations/base.py index 07f96df..a09af41 100644 --- a/configurations/base.py +++ b/configurations/base.py @@ -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 "".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) diff --git a/configurations/importer.py b/configurations/importer.py index 4997380..e3573f4 100644 --- a/configurations/importer.py +++ b/configurations/importer.py @@ -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 diff --git a/configurations/utils.py b/configurations/utils.py index 7f972e7..1f618ee 100644 --- a/configurations/utils.py +++ b/configurations/utils.py @@ -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) diff --git a/configurations/values.py b/configurations/values.py index 8f10338..8eb3bbc 100644 --- a/configurations/values.py +++ b/configurations/values.py @@ -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} diff --git a/configurations/version.py b/configurations/version.py index 10f8675..137cb24 100644 --- a/configurations/version.py +++ b/configurations/version.py @@ -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 diff --git a/configurations/wsgi.py b/configurations/wsgi.py index 54ab753..ea157a3 100644 --- a/configurations/wsgi.py +++ b/configurations/wsgi.py @@ -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() diff --git a/docs/changes.rst b/docs/changes.rst index 2c983e3..67abf0e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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) ^^^^^^^^^^^^^^^^^ diff --git a/docs/conf.py b/docs/conf.py index 5e7bd64..a0613cc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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/'), diff --git a/docs/cookbook.rst b/docs/cookbook.rst index bb5f048..21624e5 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -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 ` as the -`interface server ` +`Daphne `_ as the +`interface server `_ you have to use a asgi.py script similar to the following: .. code-block:: python diff --git a/docs/patterns.rst b/docs/patterns.rst index 6811f98..eff644a 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -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 diff --git a/docs/values.rst b/docs/values.rst index 5f5dcea..ebd21d1 100644 --- a/docs/values.rst +++ b/docs/values.rst @@ -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` 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', diff --git a/setup.cfg b/setup.cfg index 51a5b97..4c03348 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[wheel] -universal = 1 - [coverage:run] source = . branch = 1 diff --git a/setup.py b/setup.py index b032109..f311665 100644 --- a/setup.py +++ b/setup.py @@ -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', ], diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 295981c..879a3b8 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -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', diff --git a/tests/docs/conf.py b/tests/docs/conf.py index 4025988..66ce758 100644 --- a/tests/docs/conf.py +++ b/tests/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import sys diff --git a/tests/management/__init__.py b/tests/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/management/commands/__init__.py b/tests/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/management/commands/old_optparse_command.py b/tests/management/commands/old_optparse_command.py deleted file mode 100644 index d7844e5..0000000 --- a/tests/management/commands/old_optparse_command.py +++ /dev/null @@ -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 diff --git a/tests/settings/main.py b/tests/settings/main.py index 90e252f..9ea2091 100644 --- a/tests/settings/main.py +++ b/tests/settings/main.py @@ -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 diff --git a/tests/settings/mixin_inheritance.py b/tests/settings/mixin_inheritance.py index 88b4cb6..6018ac2 100644 --- a/tests/settings/mixin_inheritance.py +++ b/tests/settings/mixin_inheritance.py @@ -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 diff --git a/tests/settings/multiple_inheritance.py b/tests/settings/multiple_inheritance.py index 6eb29f3..6152e0a 100644 --- a/tests/settings/multiple_inheritance.py +++ b/tests/settings/multiple_inheritance.py @@ -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 diff --git a/tests/settings/single_inheritance.py b/tests/settings/single_inheritance.py index 34d4852..5364346 100644 --- a/tests/settings/single_inheritance.py +++ b/tests/settings/single_inheritance.py @@ -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 diff --git a/tests/setup_test.py b/tests/setup_test.py index cb156e6..59f15b1 100644 --- a/tests/setup_test.py +++ b/tests/setup_test.py @@ -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 diff --git a/tests/test_env.py b/tests/test_env.py index 47286d0..8066eea 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -1,6 +1,6 @@ import os from django.test import TestCase -from mock import patch +from unittest.mock import patch class DotEnvLoadingTests(TestCase): diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py index b40bfd3..0fe4a28 100644 --- a/tests/test_inheritance.py +++ b/tests/test_inheritance.py @@ -2,7 +2,7 @@ import os from django.test import TestCase -from mock import patch +from unittest.mock import patch class InheritanceTests(TestCase): diff --git a/tests/test_main.py b/tests/test_main.py index fe0e6ce..7cbedbe 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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 diff --git a/tests/test_sphinx.py b/tests/test_sphinx.py index 47519c4..6dc300c 100644 --- a/tests/test_sphinx.py +++ b/tests/test_sphinx.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import subprocess import os diff --git a/tests/test_values.py b/tests/test_values.py index 853b4be..2547e50 100644 --- a/tests/test_values.py +++ b/tests/test_values.py @@ -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() diff --git a/tox.ini b/tox.ini index e1df12f..57e0dde 100644 --- a/tox.ini +++ b/tox.ini @@ -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