Compare commits

...

45 commits

Author SHA1 Message Date
Christopher Broderick
3d0d4216ca
Merge pull request #399 from jazzband/dependabot/github_actions/github-actions-4ed8cc3c8a
Bump the github-actions group across 1 directory with 2 updates
2024-11-18 23:53:09 +00:00
dependabot[bot]
007161adb0
Bump the github-actions group across 1 directory with 2 updates
Bumps the github-actions group with 2 updates in the / directory: [actions/cache](https://github.com/actions/cache) and [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `actions/cache` from 3 to 4
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

Updates `codecov/codecov-action` from 4 to 5
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 21:33:41 +00:00
Christopher Broderick
57b6827ae3
Merge pull request #386 from adamchainz/pep_451
Move to PEP-451 style loader
2024-11-18 17:34:02 +00:00
Adam Johnson
b8f66f76ee Move to PEP-451 style loader 2024-11-18 16:52:55 +00:00
Christopher Broderick
fcd03ada0f
Merge pull request #398 from adamchainz/remove_env_loaded
Remove ENV_LOADED from test class
2024-11-18 16:08:45 +00:00
Christopher Broderick
0bf416155e
Merge pull request #384 from adamchainz/deprecated_form_setting
Prevent FORMS_URLFIELD_ASSUME_HTTPS warning on Django 5.0
2024-11-18 13:30:24 +00:00
Adam Johnson
cec5f7492a Remove ENV_LOADED from test class 2024-11-18 12:04:04 +00:00
Adam Johnson
b8e94fd796 Prevent FORMS_URLFIELD_ASSUME_HTTPS warning on Django 5.0 2024-11-18 11:37:17 +00:00
Christopher Broderick
f37ed87d6e
Merge pull request #393 from cclauss/patch-2
Keep GitHub Actions up to date with GitHub's Dependabot
2024-11-09 08:44:30 +00:00
Christopher Broderick
7f0f29d161
Merge pull request #389 from cclauss/patch-1
GitHub Actions: Upgrade the release workflow to Python 3.13
2024-11-08 19:31:36 +00:00
Christian Clauss
9688dae5e1
setup.py: Programming Language :: Python :: 3.13 2024-11-08 20:15:28 +01:00
Christian Clauss
4efa08f81a GitHub Actions: Upgrade to Python 3.12
https://github.com/actions/setup-python/releases
2024-11-08 18:25:41 +01:00
Christian Clauss
c67eab3507
Keep GitHub Actions up to date with GitHub's Dependabot
Fixes software supply chain safety warnings like at the bottom right of

https://github.com/jazzband/django-configurations/actions/runs/11743152917
* [Keeping your actions up to date with Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot)
* [Configuration options for the dependabot.yml file - package-ecosystem](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem)
2024-11-08 15:13:38 +01:00
Christopher Broderick
711fa66654
Merge pull request #392 from jazzband/fix_code_coverage_and_newly_failing_test
Fix code coverage and newly failing test
2024-11-08 13:36:11 +00:00
Christopher Broderick
448c5ab1b6 Address flake8 issues 2024-11-07 13:33:00 +00:00
Christopher Broderick
65d326a95a Add code coverage token param 2024-11-07 09:59:51 +00:00
Christopher Broderick
63cac8d54e Fix failing test due to external library change 2024-10-28 20:55:21 +00:00
Christopher Broderick
ba24d3c324 Fixes to accomodate v4 codecov/codecov-action changes 2024-10-28 20:54:40 +00:00
Christopher Broderick
e1091160b1
Merge pull request #383 from kloczek/master
really drop support for python<=3.7
2024-09-27 10:52:36 +01:00
Arkadiusz Adamski
880484d3b7 Update README.rst
To be consistent with previous examples.
2024-09-13 17:38:31 +02:00
Kamil Paduszyński
ef4f49d236 Fix #374 -- Fix URL in Configuration.load_dotenv docstring 2024-09-13 17:36:59 +02:00
Tomasz Kłoczko
6dc2340dfe really drop support for python<=3.7
Filer all code over `pyupgrade --py38`.

Signed-off-by: Tomasz Kłoczko <kloczek@github.com>
2024-03-18 16:08:42 +00:00
Paolo Melchiorre
ffe979b63c Fix #372 -- Add support for Python 3.12 2023-11-30 10:53:39 +01:00
Paolo Melchiorre
eba6e2c6d9
Fix #375 -- Add Django 5.0 classifier (#376) 2023-11-28 09:26:45 +01:00
Paolo Melchiorre
38641a9dea Release v2.5 2023-10-20 21:15:30 +02:00
Camilo Nova
9b7bd34812
Merge pull request #369 from pauloxnet/ticket_368
Fix #368 -- Update Python and Django versions
2023-10-16 09:19:10 -05:00
Paolo Melchiorre
df2a7f18fd
Fix #368 -- Update Python and Django versions 2023-10-04 16:52:16 +02:00
Michal Szczesny
27f67a58a4 Replace imp with importlib
This project uses the imp module which has been deprecated since Python 3.4 and set for removal in 3.12:
• Raised PendingDeprecationWarning since 3.4 (2014)
• Raised DeprecationWarning since 3.5 (2015)
• Updated DeprecationWarning to say removal in 3.12 since 3.10 (2021)
• Removal planned for 3.12 (2023)

This change removes the dependency on imp in favour of importlib.

Co-authored-by: @jbkkd
Inspired by: @mgorny

https://github.com/jazzband/django-configurations/issues/358
2023-09-27 14:08:16 +03:00
Michal Szczesny
dce5f37a93
Merge pull request #366 from washeck/master
Add pypy 3.10 to the testing matrix
2023-09-27 11:31:26 +01:00
Vaclav Rehak
2278975744 Add pypy 3.10 to the testing matrix 2023-09-22 17:55:59 +02:00
Paolo Melchiorre
cad6dcb7f0
Fix #355 - Update to Django 4.2 stable (#356) 2023-04-04 12:56:38 +02:00
Paolo Melchiorre
adaf92085f Fix #353 Improve code blocks in documentation 2023-02-15 07:50:29 +01:00
Paolo Melchiorre
a1f072ebf3
Fix #351 Use 'furo' as Sphinx theme (#352) 2023-02-14 16:01:22 +01:00
Ran Benita
6f47271526
Fix "STATICFILES_STORAGE/STORAGES are mutually exclusive" on Django>=4.2 (#349) 2023-02-13 09:18:38 +01:00
Camilo Nova
befe7f1e0d
Merge pull request #346 from pauloxnet/feature/actions-update
Update github actions and fix tests
2023-01-20 12:52:54 -06:00
Paolo Melchiorre
352d95b2ab
Update github actions and fix pipeline 2023-01-19 18:06:05 +01:00
Paolo Melchiorre
1f8bac5ba4 Fixed #344 - Run tests on python 3.11 2022-11-04 18:52:34 +01:00
Camilo Nova
17ca033d33
Merge pull request #342 from timgates42/bugfix_typos
docs: Fix a few typos
2022-09-22 17:04:19 -05:00
Camilo Nova
7520ae4123
Merge pull request #341 from michael-k/fix-release-workflow
Pin publish action in release workflow
2022-09-22 17:03:41 -05:00
Tim Gates
ac5408d7eb
docs: Fix a few typos
There are small typos in:
- configurations/base.py
- configurations/values.py
- docs/patterns.rst

Fixes:
- Should read `whether` rather than `wether`.
- Should read `overridden` rather than `overriden`.
- Should read `environment` rather than `enviroment`.

Signed-off-by: Tim Gates <tim.gates@iress.com>
2022-09-01 20:41:53 +10:00
Michael Käufl
e2861e6327
Pin publish action in release workflow
From https://github.com/pypa/gh-action-pypi-publish/blob/unstable/v1/README.md:

    The `master` branch version has been sunset. Please, change the
    GitHub Action version you use from `master` to `release/v1` or use
    an exact tag, or a full Git commit SHA.
2022-08-31 17:41:17 +02:00
Paolo Melchiorre
40d244d865 Update docs/changes.rst
Co-authored-by: Michael K. <michael-k@users.noreply.github.com>
2022-08-25 10:58:39 +02:00
Paolo Melchiorre
8923ef8c49 Prepare next release 2.4 2022-08-25 10:58:39 +02:00
Andrii Oriekhov
7e473d0f9b
add GitHub URL for PyPi (#331) 2022-08-05 16:43:17 +02:00
Paolo Melchiorre
794b858548
Fixed #336 -- Update Python and Django versions (#337) 2022-08-05 16:42:58 +02:00
27 changed files with 296 additions and 181 deletions

13
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,13 @@
# 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@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: 3.8
python-version: 3.x
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v2
uses: actions/cache@v4
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 -U pip
python -m pip install -U setuptools twine wheel
python -m pip install --upgrade pip
python -m pip install --upgrade 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@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/django-configurations/upload
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: 5
max-parallel: 6
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.6', 'pypy-3.7', 'pypy-3.8']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
@ -40,14 +40,18 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade tox tox-gh-actions
python -m pip install --upgrade "tox<4" "tox-gh-actions<3"
- name: Tox tests
run: |
tox -v
tox --verbose
- name: Upload coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v5
with:
name: Python ${{ matrix.python-version }}
name: coverage-data-${{ matrix.python-version }}
path: ".coverage.*"
include-hidden-files: true
merge-multiple: true
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -1,9 +1,9 @@
---
version: 2
build:
os: ubuntu-20.04
os: ubuntu-22.04
tools:
python: "3.9"
python: "3.10"
python:
install:
- requirements: docs/requirements.txt

View file

@ -1,4 +1,4 @@
Copyright (c) 2012-2021, Jannis Leidel and other contributors.
Copyright (c) 2012-2023, 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
pip install django-configurations
$ python -m pip install django-configurations
or, alternatively, if you want to use URL-based values:
.. code-block:: console
pip install django-configurations[cache,database,email,search]
$ python -m 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 manage.py runserver --settings=mysite.settings --configuration=Dev
$ python -m manage runserver --settings=mysite.settings --configuration=Dev
To enable Django to use your configuration you now have to modify your
**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,12 +46,22 @@ 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]
@ -60,7 +70,7 @@ class ConfigurationBase(type):
return super().__new__(cls, name, bases, attrs)
def __repr__(self):
return "<Configuration '{0}.{1}'>".format(self.__module__,
return "<Configuration '{}.{}'>".format(self.__module__,
self.__name__)
@ -97,10 +107,10 @@ class Configuration(metaclass=ConfigurationBase):
environment variables from a .env file located in the project root
or provided directory.
http://www.wellfireinteractive.com/blog/easier-12-factor-django/
https://wellfire.co/learn/easier-12-factor-django/
https://gist.github.com/bennylope/2999704
"""
# check if the class has DOTENV set wether with a path or None
# check if the class has DOTENV set whether with a path or None
dotenv = getattr(cls, 'DOTENV', None)
# if DOTENV is falsy we want to disable it
@ -109,7 +119,7 @@ class Configuration(metaclass=ConfigurationBase):
# now check if we can access the file since we know we really want to
try:
with open(dotenv, 'r') as f:
with open(dotenv) as f:
content = f.read()
except OSError as e:
raise ImproperlyConfigured("Couldn't read .env file "

View file

@ -1,4 +1,4 @@
import imp
from importlib.machinery import PathFinder
import logging
import os
import sys
@ -46,12 +46,12 @@ def install(check_options=False):
return parser
base.BaseCommand.create_parser = create_parser
importer = ConfigurationImporter(check_options=check_options)
importer = ConfigurationFinder(check_options=check_options)
sys.meta_path.insert(0, importer)
installed = True
class ConfigurationImporter:
class ConfigurationFinder(PathFinder):
modvar = SETTINGS_ENVIRONMENT_VARIABLE
namevar = CONFIGURATION_ENVIRONMENT_VARIABLE
error_msg = ("Configuration cannot be imported, "
@ -70,7 +70,7 @@ class ConfigurationImporter:
self.announce()
def __repr__(self):
return "<ConfigurationImporter for '{0}.{1}'>".format(self.module,
return "<ConfigurationFinder for '{}.{}'>".format(self.module,
self.name)
@property
@ -121,58 +121,60 @@ class ConfigurationImporter:
if (self.argv[1] == 'runserver'
and os.environ.get('RUN_MAIN') == 'true'):
message = ("django-configurations version {0}, using "
"configuration {1}".format(__version__ or "",
message = ("django-configurations version {}, using "
"configuration {}".format(__version__ or "",
self.name))
self.logger.debug(stylize(message))
def find_module(self, fullname, path=None):
def find_spec(self, fullname, path=None, target=None):
if fullname is not None and fullname == self.module:
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
spec = super().find_spec(fullname, path, target)
if spec is not None:
wrap_loader(spec.loader, self.name)
return spec
else:
mod = imp.load_module(fullname, *self.location)
cls_path = '{0}.{1}'.format(mod.__name__, self.name)
return None
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)
setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(fullname,
self.name))
cls.post_setup()
def wrap_loader(loader, class_name):
class ConfigurationLoader(loader.__class__):
def exec_module(self, module):
super().exec_module(module)
except Exception as err:
reraise(err, "Couldn't setup configuration '{0}'".format(cls_path))
mod = module
return mod
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

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("{0}{1} doesn't look like "
raise ImproperlyConfigured("{}{} doesn't look like "
"a module path".format(error_prefix,
dotted_path))
try:
module = import_module(module_path)
except ImportError as err:
msg = '{0}Error importing module {1}: "{2}"'.format(error_prefix,
msg = '{}Error importing module {}: "{}"'.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('{0}Module "{1}" does not define a '
'"{2}" attribute/class'.format(error_prefix,
raise ImproperlyConfigured('{}Module "{}" does not define a '
'"{}" 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 = ('{0} {1} {2}'.format(prefix, args[0], suffix),) + args[1:]
exc.args = (f'{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 = '{0}_{1}'.format(self.environ_prefix, environ_name)
environ_name = f'{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 {0!r} is required to be set as the '
'environment variable {1!r}'
raise ValueError('Value {!r} is required to be set as the '
'environment variable {!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 overriden when subclassing.
This should be overridden 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 {0!r} is not a '
raise ValueError('Default value {!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 {0!r}'.format(value))
'boolean value {!r}'.format(value))
class CastingMixin:
@ -152,12 +152,12 @@ class CastingMixin:
try:
self._caster = import_string(self.caster)
except ImportError as err:
msg = "Could not import {!r}".format(self.caster)
msg = f"Could not import {self.caster!r}"
raise ImproperlyConfigured(msg) from err
elif callable(self.caster):
self._caster = self.caster
else:
error = 'Cannot use caster of {0} ({1!r})'.format(self,
error = 'Cannot use caster of {} ({!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 = "Could not import {!r}".format(self.validator)
msg = f"Could not import {self.validator!r}"
raise ImproperlyConfigured(msg) from err
elif callable(self.validator):
self._validator = self.validator
else:
raise ValueError('Cannot use validator of '
'{0} ({1!r})'.format(self, self.validator))
'{} ({!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('Path {0!r} does not exist.'.format(value))
raise ValueError(f'Path {value!r} does not exist.')
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('Secret value {0!r} is not set'.format(name))
raise ValueError(f'Secret value {name!r} is not set')
return value

View file

@ -1,7 +1,4 @@
try:
from importlib.metadata import PackageNotFoundError, version
except ImportError:
from importlib_metadata import PackageNotFoundError, version
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("django-configurations")

View file

@ -3,9 +3,44 @@
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)
^^^^^^^^^^^^^^^^^
- Add compatibility with Django 4.1
- **BACKWARD INCOMPATIBLE** Drop compatibility for Django < 3.2
- **BACKWARD INCOMPATIBLE** Drop compatibility for Python 3.6
v2.3.2 (2022-01-25)
^^^^^^^^^^^^^^^^^^^

View file

@ -2,7 +2,7 @@ import configurations
# -- Project information -----------------------------------------------------
project = 'django-configurations'
copyright = '2012-2021, Jannis Leidel and other contributors'
copyright = '2012-2023, 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 = 'sphinx_rtd_theme'
html_theme = 'furo'
# -- Options for Epub output ---------------------------------------------------
epub_title = project

View file

@ -74,7 +74,7 @@ Example:
.. code-block:: console
$ tree mysite_env/
$ tree --noreport mysite_env/
mysite_env/
├── DJANGO_SETTINGS_MODULE
├── DJANGO_DEBUG
@ -82,10 +82,8 @@ 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.:
@ -151,13 +149,13 @@ First install Django 1.8.x and django-configurations:
.. code-block:: console
$ pip install -r https://raw.github.com/jazzband/django-configurations/templates/1.8.x/requirements.txt
$ python -m pip install -r https://raw.github.com/jazzband/django-configurations/templates/1.8.x/requirements.txt
Or Django 1.8:
.. code-block:: console
$ django-admin.py startproject mysite -v2 --template https://github.com/jazzband/django-configurations/archive/templates/1.8.x.zip
$ python -m django 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 enviroment they are supposed to be
and various subclasses based on the environment 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 manage.py runserver
$ export DJANGO_SETTINGS_MODULE=mysite.settings
$ export DJANGO_CONFIGURATION=Prod
$ python -m manage 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 manage.py runserver --settings=mysite.settings --configuration=Prod
$ python -m manage runserver --settings=mysite.settings --configuration=Prod
Property settings
-----------------

View file

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

View file

@ -86,9 +86,11 @@ 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):
DJANGO_ROOT_URLCONF=mysite.debugging_urls gunicorn mysite.wsgi:application
.. code-block:: console
$ 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'``.
@ -125,7 +127,9 @@ 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::
context:
.. code-block:: pycon
>>> type(values.Value([]))
<class 'configurations.values.Value'>
@ -283,17 +287,21 @@ 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:
DJANGO_MONTY_PYTHONS="Terry Jones,Graham Chapman" gunicorn mysite.wsgi:application
.. code-block:: console
$ 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:
DJANGO_EMERGENCY_EMAILS="admin@mysite.net;manager@mysite.org;support@mysite.com" gunicorn mysite.wsgi:application
.. code-block:: console
$ DJANGO_EMERGENCY_EMAILS="admin@mysite.net;manager@mysite.org;support@mysite.com" gunicorn mysite.wsgi:application
.. class:: TupleValue

View file

@ -14,6 +14,9 @@ setup(
use_scm_version={"version_scheme": "post-release", "local_scheme": "dirty-tag"},
setup_requires=["setuptools_scm"],
url='https://django-configurations.readthedocs.io/',
project_urls={
'Source': 'https://github.com/jazzband/django-configurations',
},
license='BSD',
description="A helper for organizing Django settings.",
long_description=read('README.rst'),
@ -27,10 +30,9 @@ setup(
],
},
install_requires=[
'django>=2.2',
'importlib-metadata;python_version<"3.8"',
'django>=3.2',
],
python_requires='>=3.6, <4.0',
python_requires='>=3.9, <4.0',
extras_require={
'cache': ['django-cache-url'],
'database': ['dj-database-url'],
@ -46,21 +48,22 @@ setup(
classifiers=[
'Development Status :: 5 - Production/Stable',
'Framework :: Django',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.1',
'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.6',
'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,3 +6,6 @@ class DotEnvConfiguration(Configuration):
DOTENV = 'test_project/.env'
DOTENV_VALUE = values.Value()
def DOTENV_VALUE_METHOD(self):
return values.Value(environ_name="DOTENV_VALUE")

8
tests/settings/error.py Normal file
View file

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

View file

@ -1,9 +1,7 @@
import os
import uuid
import django
from configurations import Configuration, pristinemethod
from configurations.values import BooleanValue
class Test(Configuration):
@ -11,8 +9,6 @@ class Test(Configuration):
os.path.join(os.path.dirname(
os.path.abspath(__file__)), os.pardir))
ENV_LOADED = BooleanValue(False)
DEBUG = True
SITE_ID = 1
@ -36,9 +32,6 @@ 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,4 +11,5 @@ 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)

22
tests/test_error.py Normal file
View file

@ -0,0 +1,22 @@
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 ConfigurationImporter
from configurations.importer import ConfigurationFinder
ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
TEST_PROJECT_DIR = os.path.join(ROOT_DIR, 'test_project')
@ -42,12 +42,14 @@ class MainTests(TestCase):
@patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test')
def test_empty_module_var(self):
self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
with self.assertRaises(ImproperlyConfigured):
ConfigurationFinder()
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main')
def test_empty_class_var(self):
self.assertRaises(ImproperlyConfigured, ConfigurationImporter)
with self.assertRaises(ImproperlyConfigured):
ConfigurationFinder()
def test_global_settings(self):
from configurations.base import Configuration
@ -70,21 +72,21 @@ class MainTests(TestCase):
DJANGO_SETTINGS_MODULE='tests.settings.main',
DJANGO_CONFIGURATION='Test')
def test_initialization(self):
importer = ConfigurationImporter()
self.assertEqual(importer.module, 'tests.settings.main')
self.assertEqual(importer.name, 'Test')
finder = ConfigurationFinder()
self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(finder.name, 'Test')
self.assertEqual(
repr(importer),
"<ConfigurationImporter for 'tests.settings.main.Test'>")
repr(finder),
"<ConfigurationFinder 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):
importer = ConfigurationImporter()
self.assertEqual(importer.module,
finder = ConfigurationFinder()
self.assertEqual(finder.module,
'tests.settings.inheritance')
self.assertEqual(importer.name, 'Inheritance')
self.assertEqual(finder.name, 'Inheritance')
@patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main',
@ -93,12 +95,12 @@ class MainTests(TestCase):
'--settings=tests.settings.main',
'--configuration=Test'])
def test_configuration_option(self):
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')
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')
def test_configuration_argument_in_cli(self):
"""

View file

@ -2,6 +2,7 @@ import decimal
import os
from contextlib import contextmanager
from django import VERSION as DJANGO_VERSION
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
@ -33,7 +34,7 @@ class ValueTests(TestCase):
def test_value_with_default(self):
value = Value('default', environ=False)
self.assertEqual(type(value), type('default'))
self.assertEqual(type(value), str)
self.assertEqual(value, 'default')
self.assertEqual(str(value), 'default')
@ -43,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), type('override'))
self.assertEqual(type(value), str)
self.assertEqual(value, 'override')
self.assertEqual(str(value), 'override')
self.assertEqual('{0}'.format(value), 'override')
self.assertEqual(f'{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('{0}'.format(value), 'override')
self.assertEqual(f'{value}', 'override')
self.assertEqual('%s' % value, 'override')
self.assertEqual(repr(value), repr('override'))
@ -372,16 +373,23 @@ class ValueTests(TestCase):
value = DatabaseURLValue()
self.assertEqual(value.default, {})
with env(DATABASE_URL='sqlite://'):
self.assertEqual(value.setup('DATABASE_URL'), {
'default': {
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,
'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):
@ -411,6 +419,7 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True})
with env(EMAIL_URL='console://'):
@ -421,6 +430,7 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': None,
'EMAIL_HOST_USER': None,
'EMAIL_PORT': None,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': False})
with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:wrong'): # noqa: E501
@ -429,7 +439,7 @@ class ValueTests(TestCase):
def test_cache_url_value(self):
cache_setting = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'BACKEND': 'django_redis.cache.RedisCache' if DJANGO_VERSION < (4,) else 'django.core.cache.backends.redis.RedisCache', # noqa: E501
'LOCATION': 'redis://host:6379/1',
}
}
@ -503,6 +513,7 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True
})

33
tox.ini
View file

@ -3,22 +3,22 @@ skipsdist = true
usedevelop = true
minversion = 1.8
envlist =
py36-checkqa
py311-checkqa
docs
py{36,37,py36,py37}-dj{22,31,32}
py{38,39,py38}-dj{22,31,32,40,main}
py{310}-dj{32,40,main}
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}
[gh-actions]
python =
3.6: py36,flake8,readme
3.7: py37
3.8: py38
3.9: py39
3.10: py310
pypy-3.6: pypy36
pypy-3.7: pypy37
pypy-3.8: pypy38
3.11: py311,flake8,readme
3.12: py312
3.13: py313
pypy-3.10: pypy310
[testenv]
usedevelop = true
@ -27,11 +27,16 @@ setenv =
DJANGO_CONFIGURATION = Test
COVERAGE_PROCESS_START = {toxinidir}/setup.cfg
deps =
dj22: django~=2.2.17
dj31: django~=3.1.3
dj32: django~=3.2.9
dj40: django~=4.0.0
dj41: django~=4.1.3
dj42: django~=4.2.0
dj50: django~=5.0.0
dj51: django~=5.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
@ -42,7 +47,7 @@ commands =
coverage report -m --skip-covered
coverage xml
[testenv:py36-checkqa]
[testenv:py311-checkqa]
commands =
flake8 {toxinidir}
check-manifest -v