mirror of
https://github.com/jazzband/django-configurations.git
synced 2026-03-17 06:30:28 +00:00
Compare commits
No commits in common. "master" and "2.4" have entirely different histories.
27 changed files with 173 additions and 279 deletions
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
|
|
@ -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
|
||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
|
|
@ -11,22 +11,22 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.x
|
||||
python-version: 3.8
|
||||
|
||||
- name: Get pip cache dir
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key: release-${{ hashFiles('**/setup.py') }}
|
||||
|
|
@ -35,8 +35,8 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade setuptools twine wheel
|
||||
python -m pip install -U pip
|
||||
python -m pip install -U setuptools twine wheel
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
|
|
@ -46,8 +46,8 @@ jobs:
|
|||
|
||||
- name: Upload packages to Jazzband
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
user: jazzband
|
||||
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
|
||||
repository-url: https://jazzband.co/projects/django-configurations/upload
|
||||
repository_url: https://jazzband.co/projects/django-configurations/upload
|
||||
|
|
|
|||
24
.github/workflows/test.yml
vendored
24
.github/workflows/test.yml
vendored
|
|
@ -11,25 +11,25 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 6
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10']
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy-3.7', 'pypy-3.8', 'pypy-3.9']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Get pip cache dir
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key:
|
||||
|
|
@ -40,18 +40,14 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade "tox<4" "tox-gh-actions<3"
|
||||
python -m pip install --upgrade tox tox-gh-actions
|
||||
|
||||
- name: Tox tests
|
||||
run: |
|
||||
tox --verbose
|
||||
tox -v
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
name: coverage-data-${{ matrix.python-version }}
|
||||
path: ".coverage.*"
|
||||
include-hidden-files: true
|
||||
merge-multiple: true
|
||||
name: Python ${{ matrix.python-version }}
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
version: 2
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
python: "3.9"
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2012-2023, Jannis Leidel and other contributors.
|
||||
Copyright (c) 2012-2022, Jannis Leidel and other contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
|
|
|
|||
12
README.rst
12
README.rst
|
|
@ -47,13 +47,13 @@ Install django-configurations:
|
|||
|
||||
.. code-block:: console
|
||||
|
||||
$ python -m pip install django-configurations
|
||||
pip install django-configurations
|
||||
|
||||
or, alternatively, if you want to use URL-based values:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python -m pip install django-configurations[cache,database,email,search]
|
||||
pip install django-configurations[cache,database,email,search]
|
||||
|
||||
Then subclass the included ``configurations.Configuration`` class in your
|
||||
project's **settings.py** or any other module you're using to store the
|
||||
|
|
@ -73,14 +73,14 @@ you just created, e.g. in bash:
|
|||
|
||||
.. code-block:: console
|
||||
|
||||
$ export DJANGO_CONFIGURATION=Dev
|
||||
export DJANGO_CONFIGURATION=Dev
|
||||
|
||||
and the ``DJANGO_SETTINGS_MODULE`` environment variable to the module
|
||||
import path as usual, e.g. in bash:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export DJANGO_SETTINGS_MODULE=mysite.settings
|
||||
export DJANGO_SETTINGS_MODULE=mysite.settings
|
||||
|
||||
*Alternatively* supply the ``--configuration`` option when using Django
|
||||
management commands along the lines of Django's default ``--settings``
|
||||
|
|
@ -88,7 +88,7 @@ command line option, e.g.
|
|||
|
||||
.. code-block:: console
|
||||
|
||||
$ python -m manage runserver --settings=mysite.settings --configuration=Dev
|
||||
python manage.py runserver --settings=mysite.settings --configuration=Dev
|
||||
|
||||
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
|
||||
|
|
@ -137,7 +137,7 @@ Or if you are not serving your app via WSGI but ASGI instead, you need to modify
|
|||
import os
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
|
||||
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
|
||||
os.environ.setdefault('DJANGO_CONFIGURATION', 'DEV')
|
||||
|
||||
from configurations.asgi import get_asgi_application
|
||||
|
||||
|
|
|
|||
|
|
@ -46,22 +46,12 @@ class ConfigurationBase(type):
|
|||
# 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]
|
||||
|
|
@ -70,7 +60,7 @@ class ConfigurationBase(type):
|
|||
return super().__new__(cls, name, bases, attrs)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Configuration '{}.{}'>".format(self.__module__,
|
||||
return "<Configuration '{0}.{1}'>".format(self.__module__,
|
||||
self.__name__)
|
||||
|
||||
|
||||
|
|
@ -107,10 +97,10 @@ class Configuration(metaclass=ConfigurationBase):
|
|||
environment variables from a .env file located in the project root
|
||||
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
|
||||
"""
|
||||
# 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)
|
||||
|
||||
# if DOTENV is falsy we want to disable it
|
||||
|
|
@ -119,7 +109,7 @@ class Configuration(metaclass=ConfigurationBase):
|
|||
|
||||
# now check if we can access the file since we know we really want to
|
||||
try:
|
||||
with open(dotenv) as f:
|
||||
with open(dotenv, 'r') as f:
|
||||
content = f.read()
|
||||
except OSError as e:
|
||||
raise ImproperlyConfigured("Couldn't read .env file "
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from importlib.machinery import PathFinder
|
||||
import imp
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -46,12 +46,12 @@ def install(check_options=False):
|
|||
return 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)
|
||||
installed = True
|
||||
|
||||
|
||||
class ConfigurationFinder(PathFinder):
|
||||
class ConfigurationImporter:
|
||||
modvar = SETTINGS_ENVIRONMENT_VARIABLE
|
||||
namevar = CONFIGURATION_ENVIRONMENT_VARIABLE
|
||||
error_msg = ("Configuration cannot be imported, "
|
||||
|
|
@ -70,7 +70,7 @@ class ConfigurationFinder(PathFinder):
|
|||
self.announce()
|
||||
|
||||
def __repr__(self):
|
||||
return "<ConfigurationFinder for '{}.{}'>".format(self.module,
|
||||
return "<ConfigurationImporter for '{0}.{1}'>".format(self.module,
|
||||
self.name)
|
||||
|
||||
@property
|
||||
|
|
@ -121,60 +121,58 @@ class ConfigurationFinder(PathFinder):
|
|||
if (self.argv[1] == 'runserver'
|
||||
and os.environ.get('RUN_MAIN') == 'true'):
|
||||
|
||||
message = ("django-configurations version {}, using "
|
||||
"configuration {}".format(__version__ or "",
|
||||
message = ("django-configurations version {0}, using "
|
||||
"configuration {1}".format(__version__ or "",
|
||||
self.name))
|
||||
self.logger.debug(stylize(message))
|
||||
|
||||
def find_spec(self, fullname, path=None, target=None):
|
||||
def find_module(self, fullname, path=None):
|
||||
if fullname is not None and fullname == self.module:
|
||||
spec = super().find_spec(fullname, path, target)
|
||||
if spec is not None:
|
||||
wrap_loader(spec.loader, self.name)
|
||||
return spec
|
||||
module = fullname.rsplit('.', 1)[-1]
|
||||
return ConfigurationLoader(self.name,
|
||||
imp.find_module(module, path))
|
||||
return None
|
||||
|
||||
|
||||
class ConfigurationLoader:
|
||||
|
||||
def __init__(self, name, location):
|
||||
self.name = name
|
||||
self.location = location
|
||||
|
||||
def load_module(self, fullname):
|
||||
if fullname in sys.modules:
|
||||
mod = sys.modules[fullname] # pragma: no cover
|
||||
else:
|
||||
return None
|
||||
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):
|
||||
class ConfigurationLoader(loader.__class__):
|
||||
def exec_module(self, module):
|
||||
super().exec_module(module)
|
||||
setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(fullname,
|
||||
self.name))
|
||||
cls.post_setup()
|
||||
|
||||
mod = module
|
||||
except Exception as err:
|
||||
reraise(err, "Couldn't setup configuration '{0}'".format(cls_path))
|
||||
|
||||
cls_path = f'{mod.__name__}.{class_name}'
|
||||
|
||||
try:
|
||||
cls = getattr(mod, class_name)
|
||||
except AttributeError as err: # pragma: no cover
|
||||
reraise(
|
||||
err,
|
||||
(
|
||||
f"Couldn't find configuration '{class_name}' in "
|
||||
f"module '{mod.__package__}'"
|
||||
),
|
||||
)
|
||||
try:
|
||||
cls.pre_setup()
|
||||
cls.setup()
|
||||
obj = cls()
|
||||
attributes = uppercase_attributes(obj).items()
|
||||
for name, value in attributes:
|
||||
if callable(value) and not getattr(value, 'pristine', False):
|
||||
value = value()
|
||||
# in case a method returns a Value instance we have
|
||||
# to do the same as the Configuration.setup method
|
||||
if isinstance(value, Value):
|
||||
setup_value(mod, name, value)
|
||||
continue
|
||||
setattr(mod, name, value)
|
||||
|
||||
setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(module.__name__,
|
||||
class_name))
|
||||
cls.post_setup()
|
||||
|
||||
except Exception as err:
|
||||
reraise(err, f"Couldn't setup configuration '{cls_path}'")
|
||||
|
||||
loader.__class__ = ConfigurationLoader
|
||||
return mod
|
||||
|
|
|
|||
|
|
@ -29,21 +29,21 @@ def import_by_path(dotted_path, error_prefix=''):
|
|||
try:
|
||||
module_path, class_name = dotted_path.rsplit('.', 1)
|
||||
except ValueError:
|
||||
raise ImproperlyConfigured("{}{} doesn't look like "
|
||||
raise ImproperlyConfigured("{0}{1} doesn't look like "
|
||||
"a module path".format(error_prefix,
|
||||
dotted_path))
|
||||
try:
|
||||
module = import_module(module_path)
|
||||
except ImportError as err:
|
||||
msg = '{}Error importing module {}: "{}"'.format(error_prefix,
|
||||
msg = '{0}Error importing module {1}: "{2}"'.format(error_prefix,
|
||||
module_path,
|
||||
err)
|
||||
raise ImproperlyConfigured(msg).with_traceback(sys.exc_info()[2])
|
||||
try:
|
||||
attr = getattr(module, class_name)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('{}Module "{}" does not define a '
|
||||
'"{}" attribute/class'.format(error_prefix,
|
||||
raise ImproperlyConfigured('{0}Module "{1}" does not define a '
|
||||
'"{2}" attribute/class'.format(error_prefix,
|
||||
module_path,
|
||||
class_name))
|
||||
return attr
|
||||
|
|
@ -61,7 +61,7 @@ def reraise(exc, prefix=None, suffix=None):
|
|||
suffix = ''
|
||||
elif not (suffix.startswith('(') and suffix.endswith(')')):
|
||||
suffix = '(' + suffix + ')'
|
||||
exc.args = (f'{prefix} {args[0]} {suffix}',) + args[1:]
|
||||
exc.args = ('{0} {1} {2}'.format(prefix, args[0], suffix),) + args[1:]
|
||||
raise exc
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class Value:
|
|||
else:
|
||||
environ_name = name.upper()
|
||||
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
|
||||
|
||||
def setup(self, name):
|
||||
|
|
@ -102,8 +102,8 @@ class Value:
|
|||
if full_environ_name in os.environ:
|
||||
value = self.to_python(os.environ[full_environ_name])
|
||||
elif self.environ_required:
|
||||
raise ValueError('Value {!r} is required to be set as the '
|
||||
'environment variable {!r}'
|
||||
raise ValueError('Value {0!r} is required to be set as the '
|
||||
'environment variable {1!r}'
|
||||
.format(name, full_environ_name))
|
||||
self.value = value
|
||||
return value
|
||||
|
|
@ -112,7 +112,7 @@ class Value:
|
|||
"""
|
||||
Convert the given value of a environment variable into an
|
||||
appropriate Python representation of the value.
|
||||
This should be overridden when subclassing.
|
||||
This should be overriden when subclassing.
|
||||
"""
|
||||
return value
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ class BooleanValue(Value):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
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))
|
||||
|
||||
def to_python(self, value):
|
||||
|
|
@ -139,7 +139,7 @@ class BooleanValue(Value):
|
|||
return False
|
||||
else:
|
||||
raise ValueError('Cannot interpret '
|
||||
'boolean value {!r}'.format(value))
|
||||
'boolean value {0!r}'.format(value))
|
||||
|
||||
|
||||
class CastingMixin:
|
||||
|
|
@ -152,12 +152,12 @@ class CastingMixin:
|
|||
try:
|
||||
self._caster = import_string(self.caster)
|
||||
except ImportError as err:
|
||||
msg = f"Could not import {self.caster!r}"
|
||||
msg = "Could not import {!r}".format(self.caster)
|
||||
raise ImproperlyConfigured(msg) from err
|
||||
elif callable(self.caster):
|
||||
self._caster = self.caster
|
||||
else:
|
||||
error = 'Cannot use caster of {} ({!r})'.format(self,
|
||||
error = 'Cannot use caster of {0} ({1!r})'.format(self,
|
||||
self.caster)
|
||||
raise ValueError(error)
|
||||
try:
|
||||
|
|
@ -345,13 +345,13 @@ class ValidationMixin:
|
|||
try:
|
||||
self._validator = import_string(self.validator)
|
||||
except ImportError as err:
|
||||
msg = f"Could not import {self.validator!r}"
|
||||
msg = "Could not import {!r}".format(self.validator)
|
||||
raise ImproperlyConfigured(msg) from err
|
||||
elif callable(self.validator):
|
||||
self._validator = self.validator
|
||||
else:
|
||||
raise ValueError('Cannot use validator of '
|
||||
'{} ({!r})'.format(self, self.validator))
|
||||
'{0} ({1!r})'.format(self, self.validator))
|
||||
if self.default:
|
||||
self.to_python(self.default)
|
||||
|
||||
|
|
@ -397,7 +397,7 @@ class PathValue(Value):
|
|||
value = super().setup(name)
|
||||
value = os.path.expanduser(value)
|
||||
if self.check_exists and not os.path.exists(value):
|
||||
raise ValueError(f'Path {value!r} does not exist.')
|
||||
raise ValueError('Path {0!r} does not exist.'.format(value))
|
||||
return os.path.abspath(value)
|
||||
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ class SecretValue(Value):
|
|||
def setup(self, name):
|
||||
value = super().setup(name)
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
from importlib.metadata import PackageNotFoundError, version
|
||||
try:
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
except ImportError:
|
||||
from importlib_metadata import PackageNotFoundError, version
|
||||
|
||||
try:
|
||||
__version__ = version("django-configurations")
|
||||
|
|
|
|||
|
|
@ -3,37 +3,9 @@
|
|||
Changelog
|
||||
---------
|
||||
|
||||
Unreleased
|
||||
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)
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import configurations
|
|||
|
||||
# -- Project information -----------------------------------------------------
|
||||
project = 'django-configurations'
|
||||
copyright = '2012-2023, Jannis Leidel and other contributors'
|
||||
copyright = '2012-2022, Jannis Leidel and other contributors'
|
||||
author = 'Jannis Leidel and other contributors'
|
||||
|
||||
release = configurations.__version__
|
||||
|
|
@ -28,7 +28,7 @@ intersphinx_mapping = {
|
|||
}
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
html_theme = 'furo'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# -- Options for Epub output ---------------------------------------------------
|
||||
epub_title = project
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ Example:
|
|||
|
||||
.. code-block:: console
|
||||
|
||||
$ tree --noreport mysite_env/
|
||||
$ tree mysite_env/
|
||||
mysite_env/
|
||||
├── DJANGO_SETTINGS_MODULE
|
||||
├── DJANGO_DEBUG
|
||||
|
|
@ -82,8 +82,10 @@ Example:
|
|||
├── DJANGO_CACHE_URL
|
||||
└── PYTHONSTARTUP
|
||||
|
||||
0 directories, 3 files
|
||||
$ cat mysite_env/DJANGO_CACHE_URL
|
||||
redis://user@host:port/1
|
||||
$
|
||||
|
||||
Then, to enable the ``mysite_env`` environment variables, simply use the
|
||||
``envdir`` command line tool as a prefix for your program, e.g.:
|
||||
|
|
@ -149,13 +151,13 @@ First install Django 1.8.x and django-configurations:
|
|||
|
||||
.. code-block:: console
|
||||
|
||||
$ python -m pip install -r https://raw.github.com/jazzband/django-configurations/templates/1.8.x/requirements.txt
|
||||
$ pip install -r https://raw.github.com/jazzband/django-configurations/templates/1.8.x/requirements.txt
|
||||
|
||||
Or Django 1.8:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python -m django startproject mysite -v2 --template https://github.com/jazzband/django-configurations/archive/templates/1.8.x.zip
|
||||
$ 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``
|
||||
directory that uses django-configurations.
|
||||
|
|
|
|||
|
|
@ -93,6 +93,6 @@ Bugs and feature requests
|
|||
As always your mileage may vary, so please don't hesitate to send feature
|
||||
requests and bug reports:
|
||||
|
||||
- https://github.com/jazzband/django-configurations/issues
|
||||
https://github.com/jazzband/django-configurations/issues
|
||||
|
||||
Thanks!
|
||||
|
|
@ -3,7 +3,7 @@ Usage patterns
|
|||
|
||||
There are various configuration patterns that can be implemented with
|
||||
django-configurations. The most common pattern is to have a base class
|
||||
and various subclasses based on the environment they are supposed to be
|
||||
and various subclasses based on the enviroment they are supposed to be
|
||||
used in, e.g. in production, staging and development.
|
||||
|
||||
Server specific settings
|
||||
|
|
@ -31,9 +31,9 @@ it should be ``Prod``. In Bash that would be:
|
|||
|
||||
.. code-block:: console
|
||||
|
||||
$ export DJANGO_SETTINGS_MODULE=mysite.settings
|
||||
$ export DJANGO_CONFIGURATION=Prod
|
||||
$ python -m manage runserver
|
||||
export DJANGO_SETTINGS_MODULE=mysite.settings
|
||||
export DJANGO_CONFIGURATION=Prod
|
||||
python manage.py runserver
|
||||
|
||||
Alternatively you can use the ``--configuration`` option when using Django
|
||||
management commands along the lines of Django's default ``--settings``
|
||||
|
|
@ -41,7 +41,7 @@ command line option, e.g.
|
|||
|
||||
.. code-block:: console
|
||||
|
||||
$ python -m manage runserver --settings=mysite.settings --configuration=Prod
|
||||
python manage.py runserver --settings=mysite.settings --configuration=Prod
|
||||
|
||||
Property settings
|
||||
-----------------
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
Sphinx>4
|
||||
furo
|
||||
docutils
|
||||
sphinx-rtd-theme
|
||||
docutils<0.18 # https://github.com/readthedocs/readthedocs.org/issues/8616
|
||||
|
|
|
|||
|
|
@ -86,11 +86,9 @@ prefixed with ``DJANGO_``. E.g.:
|
|||
django-configurations will try to read the ``DJANGO_ROOT_URLCONF`` environment
|
||||
variable when deciding which value the ``ROOT_URLCONF`` setting should have.
|
||||
When you run the web server simply specify that environment variable
|
||||
(e.g. in your init script):
|
||||
(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
|
||||
``'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
|
||||
directly converted to its final value for use outside of the configuration
|
||||
context:
|
||||
|
||||
.. code-block:: pycon
|
||||
context::
|
||||
|
||||
>>> type(values.Value([]))
|
||||
<class 'configurations.values.Value'>
|
||||
|
|
@ -287,21 +283,17 @@ Type values
|
|||
MONTY_PYTHONS = ListValue(['John Cleese', 'Eric Idle'],
|
||||
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::
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
12
setup.py
12
setup.py
|
|
@ -31,8 +31,9 @@ setup(
|
|||
},
|
||||
install_requires=[
|
||||
'django>=3.2',
|
||||
'importlib-metadata;python_version<"3.8"',
|
||||
],
|
||||
python_requires='>=3.9, <4.0',
|
||||
python_requires='>=3.7, <4.0',
|
||||
extras_require={
|
||||
'cache': ['django-cache-url'],
|
||||
'database': ['dj-database-url'],
|
||||
|
|
@ -49,21 +50,18 @@ setup(
|
|||
'Development Status :: 5 - Production/Stable',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 3.2',
|
||||
'Framework :: Django :: 4.0',
|
||||
'Framework :: Django :: 4.1',
|
||||
'Framework :: Django :: 4.2',
|
||||
'Framework :: Django :: 5.0',
|
||||
'Framework :: Django :: 5.1',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
'Programming Language :: Python :: 3.13',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,3 @@ class DotEnvConfiguration(Configuration):
|
|||
DOTENV = 'test_project/.env'
|
||||
|
||||
DOTENV_VALUE = values.Value()
|
||||
|
||||
def DOTENV_VALUE_METHOD(self):
|
||||
return values.Value(environ_name="DOTENV_VALUE")
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
from configurations import Configuration
|
||||
|
||||
|
||||
class ErrorConfiguration(Configuration):
|
||||
|
||||
@classmethod
|
||||
def pre_setup(cls):
|
||||
raise ValueError("Error in pre_setup")
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import os
|
||||
import uuid
|
||||
import django
|
||||
|
||||
from configurations import Configuration, pristinemethod
|
||||
from configurations.values import BooleanValue
|
||||
|
||||
|
||||
class Test(Configuration):
|
||||
|
|
@ -9,6 +11,8 @@ class Test(Configuration):
|
|||
os.path.join(os.path.dirname(
|
||||
os.path.abspath(__file__)), os.pardir))
|
||||
|
||||
ENV_LOADED = BooleanValue(False)
|
||||
|
||||
DEBUG = True
|
||||
|
||||
SITE_ID = 1
|
||||
|
|
@ -32,6 +36,9 @@ class Test(Configuration):
|
|||
|
||||
ROOT_URLCONF = 'tests.urls'
|
||||
|
||||
if django.VERSION[:2] < (1, 6):
|
||||
TEST_RUNNER = 'discover_runner.DiscoverRunner'
|
||||
|
||||
@property
|
||||
def ALLOWED_HOSTS(self):
|
||||
allowed_hosts = super().ALLOWED_HOSTS[:]
|
||||
|
|
|
|||
|
|
@ -11,5 +11,4 @@ class DotEnvLoadingTests(TestCase):
|
|||
def test_env_loaded(self):
|
||||
from tests.settings import dot_env
|
||||
self.assertEqual(dot_env.DOTENV_VALUE, 'is set')
|
||||
self.assertEqual(dot_env.DOTENV_VALUE_METHOD, 'is set')
|
||||
self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV)
|
||||
|
|
|
|||
|
|
@ -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 ",
|
||||
)
|
||||
)
|
||||
|
|
@ -7,7 +7,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||
|
||||
from unittest.mock import patch
|
||||
|
||||
from configurations.importer import ConfigurationFinder
|
||||
from configurations.importer import ConfigurationImporter
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
TEST_PROJECT_DIR = os.path.join(ROOT_DIR, 'test_project')
|
||||
|
|
@ -42,14 +42,12 @@ class MainTests(TestCase):
|
|||
|
||||
@patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test')
|
||||
def test_empty_module_var(self):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
ConfigurationFinder()
|
||||
self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
|
||||
|
||||
@patch.dict(os.environ, clear=True,
|
||||
DJANGO_SETTINGS_MODULE='tests.settings.main')
|
||||
def test_empty_class_var(self):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
ConfigurationFinder()
|
||||
self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
|
||||
|
||||
def test_global_settings(self):
|
||||
from configurations.base import Configuration
|
||||
|
|
@ -72,21 +70,21 @@ class MainTests(TestCase):
|
|||
DJANGO_SETTINGS_MODULE='tests.settings.main',
|
||||
DJANGO_CONFIGURATION='Test')
|
||||
def test_initialization(self):
|
||||
finder = ConfigurationFinder()
|
||||
self.assertEqual(finder.module, 'tests.settings.main')
|
||||
self.assertEqual(finder.name, 'Test')
|
||||
importer = ConfigurationImporter()
|
||||
self.assertEqual(importer.module, 'tests.settings.main')
|
||||
self.assertEqual(importer.name, 'Test')
|
||||
self.assertEqual(
|
||||
repr(finder),
|
||||
"<ConfigurationFinder for 'tests.settings.main.Test'>")
|
||||
repr(importer),
|
||||
"<ConfigurationImporter for 'tests.settings.main.Test'>")
|
||||
|
||||
@patch.dict(os.environ, clear=True,
|
||||
DJANGO_SETTINGS_MODULE='tests.settings.inheritance',
|
||||
DJANGO_CONFIGURATION='Inheritance')
|
||||
def test_initialization_inheritance(self):
|
||||
finder = ConfigurationFinder()
|
||||
self.assertEqual(finder.module,
|
||||
importer = ConfigurationImporter()
|
||||
self.assertEqual(importer.module,
|
||||
'tests.settings.inheritance')
|
||||
self.assertEqual(finder.name, 'Inheritance')
|
||||
self.assertEqual(importer.name, 'Inheritance')
|
||||
|
||||
@patch.dict(os.environ, clear=True,
|
||||
DJANGO_SETTINGS_MODULE='tests.settings.main',
|
||||
|
|
@ -95,12 +93,12 @@ class MainTests(TestCase):
|
|||
'--settings=tests.settings.main',
|
||||
'--configuration=Test'])
|
||||
def test_configuration_option(self):
|
||||
finder = ConfigurationFinder(check_options=False)
|
||||
self.assertEqual(finder.module, 'tests.settings.main')
|
||||
self.assertEqual(finder.name, 'NonExisting')
|
||||
finder = ConfigurationFinder(check_options=True)
|
||||
self.assertEqual(finder.module, 'tests.settings.main')
|
||||
self.assertEqual(finder.name, 'Test')
|
||||
importer = ConfigurationImporter(check_options=False)
|
||||
self.assertEqual(importer.module, 'tests.settings.main')
|
||||
self.assertEqual(importer.name, 'NonExisting')
|
||||
importer = ConfigurationImporter(check_options=True)
|
||||
self.assertEqual(importer.module, 'tests.settings.main')
|
||||
self.assertEqual(importer.name, 'Test')
|
||||
|
||||
def test_configuration_argument_in_cli(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class ValueTests(TestCase):
|
|||
|
||||
def test_value_with_default(self):
|
||||
value = Value('default', environ=False)
|
||||
self.assertEqual(type(value), str)
|
||||
self.assertEqual(type(value), type('default'))
|
||||
self.assertEqual(value, 'default')
|
||||
self.assertEqual(str(value), 'default')
|
||||
|
||||
|
|
@ -44,17 +44,17 @@ class ValueTests(TestCase):
|
|||
with env(DJANGO_TEST='override'):
|
||||
self.assertEqual(value.setup('TEST'), 'default')
|
||||
value = Value(environ_name='TEST')
|
||||
self.assertEqual(type(value), str)
|
||||
self.assertEqual(type(value), type('override'))
|
||||
self.assertEqual(value, 'override')
|
||||
self.assertEqual(str(value), 'override')
|
||||
self.assertEqual(f'{value}', 'override')
|
||||
self.assertEqual('{0}'.format(value), 'override')
|
||||
self.assertEqual('%s' % value, 'override')
|
||||
|
||||
value = Value(environ_name='TEST', late_binding=True)
|
||||
self.assertEqual(type(value), Value)
|
||||
self.assertEqual(value.value, 'override')
|
||||
self.assertEqual(str(value), 'override')
|
||||
self.assertEqual(f'{value}', 'override')
|
||||
self.assertEqual('{0}'.format(value), 'override')
|
||||
self.assertEqual('%s' % value, 'override')
|
||||
|
||||
self.assertEqual(repr(value), repr('override'))
|
||||
|
|
@ -373,23 +373,16 @@ class ValueTests(TestCase):
|
|||
value = DatabaseURLValue()
|
||||
self.assertEqual(value.default, {})
|
||||
with env(DATABASE_URL='sqlite://'):
|
||||
settings_value = value.setup('DATABASE_URL')
|
||||
# Compare the embedded dicts in the "default" entry so that the difference can be seen if
|
||||
# it fails ... DatabaseURLValue(|) uses an external app that can add additional entries
|
||||
self.assertDictEqual(
|
||||
{
|
||||
'CONN_HEALTH_CHECKS': False,
|
||||
self.assertEqual(value.setup('DATABASE_URL'), {
|
||||
'default': {
|
||||
'CONN_MAX_AGE': 0,
|
||||
'DISABLE_SERVER_SIDE_CURSORS': False,
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'HOST': '',
|
||||
'NAME': ':memory:',
|
||||
'PASSWORD': '',
|
||||
'PORT': '',
|
||||
'USER': '',
|
||||
},
|
||||
settings_value['default']
|
||||
)
|
||||
}})
|
||||
|
||||
def test_database_url_additional_args(self):
|
||||
|
||||
|
|
|
|||
30
tox.ini
30
tox.ini
|
|
@ -3,22 +3,20 @@ skipsdist = true
|
|||
usedevelop = true
|
||||
minversion = 1.8
|
||||
envlist =
|
||||
py311-checkqa
|
||||
py37-checkqa
|
||||
docs
|
||||
py{39}-dj{32,41,42}
|
||||
py{310,py310}-dj{32,41,42,50,main}
|
||||
py{311}-dj{41,42,50,51,main}
|
||||
py{312}-dj{50,51,main}
|
||||
py{313}-dj{50,51,main}
|
||||
py{37,py37}-dj{32}
|
||||
py{38,py38,39,py39,310}-dj{32,40,41,main}
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.7: py37,flake8,readme
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311,flake8,readme
|
||||
3.12: py312
|
||||
3.13: py313
|
||||
pypy-3.10: pypy310
|
||||
pypy-3.7: pypy37
|
||||
pypy-3.8: pypy38
|
||||
pypy-3.9: pypy39
|
||||
|
||||
[testenv]
|
||||
usedevelop = true
|
||||
|
|
@ -28,15 +26,9 @@ setenv =
|
|||
COVERAGE_PROCESS_START = {toxinidir}/setup.cfg
|
||||
deps =
|
||||
dj32: django~=3.2.9
|
||||
dj41: django~=4.1.3
|
||||
dj42: django~=4.2.0
|
||||
dj50: django~=5.0.0
|
||||
dj51: django~=5.1.0
|
||||
dj40: django~=4.0.0
|
||||
dj41: django~=4.1.0
|
||||
djmain: https://github.com/django/django/archive/main.tar.gz
|
||||
py312: setuptools
|
||||
py312: wheel
|
||||
py313: setuptools
|
||||
py313: wheel
|
||||
coverage
|
||||
coverage_enable_subprocess
|
||||
extras = testing
|
||||
|
|
@ -47,7 +39,7 @@ commands =
|
|||
coverage report -m --skip-covered
|
||||
coverage xml
|
||||
|
||||
[testenv:py311-checkqa]
|
||||
[testenv:py37-checkqa]
|
||||
commands =
|
||||
flake8 {toxinidir}
|
||||
check-manifest -v
|
||||
|
|
|
|||
Loading…
Reference in a new issue