Compare commits

..

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

27 changed files with 173 additions and 279 deletions

View file

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

View file

@ -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

View file

@ -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 }}

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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 "

View 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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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)
^^^^^^^^^^^^^^^^^

View file

@ -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

View file

@ -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.

View file

@ -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!

View file

@ -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
-----------------

View file

@ -1,3 +1,3 @@
Sphinx>4
furo
docutils
sphinx-rtd-theme
docutils<0.18 # https://github.com/readthedocs/readthedocs.org/issues/8616

View file

@ -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

View file

@ -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',
],

View file

@ -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")

View file

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

View file

@ -1,7 +1,9 @@
import os
import 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[:]

View file

@ -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)

View file

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

View file

@ -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):
"""

View file

@ -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
View file

@ -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