Compare commits

...

53 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
Nicolas Delaby
141d8ef2c4
Merge pull request #328 from ticosax/prepare-new-release
Prepare next release 2.3.2
2022-01-25 10:27:57 +01:00
Nicolas Delaby
fcba8b6d92 Prepare next release 2.3.2 2022-01-25 09:26:07 +01:00
Nicolas Delaby
861935fd45
Merge pull request #327 from ticosax/fix-deprecated-settings
Remove deprecated default fields only from the global
2022-01-25 09:12:33 +01:00
Nicolas Delaby
5f438451ea add changelog entry 2022-01-24 18:57:41 +01:00
Nicolas Delaby
45a4557efe Remove deprecated default fields only from the global
And keep user defined values
2022-01-24 18:46:17 +01:00
Jannis Leidel
bb2523ddb9
Fix badge URL. 2021-12-23 20:36:48 +01:00
Paolo Melchiorre
32a41d62b9
Align README badges with other jazzband projects (#326) 2021-12-23 20:35:53 +01:00
Paolo Melchiorre
ded548b19b Update Django 4.0 stable versions 2021-12-23 17:23:58 +06:00
27 changed files with 336 additions and 193 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 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v5
with: with:
python-version: 3.8 python-version: 3.x
- name: Get pip cache dir - name: Get pip cache dir
id: pip-cache id: pip-cache
run: | run: |
echo "::set-output name=dir::$(pip cache dir)" echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache - name: Cache
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: ${{ steps.pip-cache.outputs.dir }} path: ${{ steps.pip-cache.outputs.dir }}
key: release-${{ hashFiles('**/setup.py') }} key: release-${{ hashFiles('**/setup.py') }}
@ -35,8 +35,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install -U pip python -m pip install --upgrade pip
python -m pip install -U setuptools twine wheel python -m pip install --upgrade setuptools twine wheel
- name: Build package - name: Build package
run: | run: |
@ -46,8 +46,8 @@ jobs:
- name: Upload packages to Jazzband - name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 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: with:
user: jazzband user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 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 runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 5 max-parallel: 6
matrix: 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Get pip cache dir - name: Get pip cache dir
id: pip-cache id: pip-cache
run: | run: |
echo "::set-output name=dir::$(pip cache dir)" echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache - name: Cache
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: ${{ steps.pip-cache.outputs.dir }} path: ${{ steps.pip-cache.outputs.dir }}
key: key:
@ -40,14 +40,18 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip 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 - name: Tox tests
run: | run: |
tox -v tox --verbose
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v5
with: 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 fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -1,9 +1,9 @@
--- ---
version: 2 version: 2
build: build:
os: ubuntu-20.04 os: ubuntu-22.04
tools: tools:
python: "3.9" python: "3.10"
python: python:
install: install:
- requirements: docs/requirements.txt - 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. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,

View file

@ -1,7 +1,7 @@
django-configurations |latest-version| django-configurations |latest-version|
====================================== ======================================
|build-status| |codecov| |docs| |python-support| |jazzband| |jazzband| |build-status| |codecov| |docs| |python-support| |django-support|
django-configurations eases Django project configuration by relying django-configurations eases Django project configuration by relying
on the composability of Python classes. It extends the notion of on the composability of Python classes. It extends the notion of
@ -11,23 +11,33 @@ object oriented programming patterns.
Check out the `documentation`_ for more complete examples. Check out the `documentation`_ for more complete examples.
.. |latest-version| image:: https://img.shields.io/pypi/v/django-configurations.svg .. |latest-version| image:: https://img.shields.io/pypi/v/django-configurations.svg
:alt: Latest version on PyPI
:target: https://pypi.python.org/pypi/django-configurations :target: https://pypi.python.org/pypi/django-configurations
:alt: Latest version on PyPI
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg
:target: https://jazzband.co/
:alt: Jazzband
.. |build-status| image:: https://github.com/jazzband/django-configurations/workflows/Test/badge.svg .. |build-status| image:: https://github.com/jazzband/django-configurations/workflows/Test/badge.svg
:target: https://github.com/jazzband/django-configurations/actions :target: https://github.com/jazzband/django-configurations/actions
:alt: GitHub Actions :alt: Build Status
.. |codecov| image:: https://codecov.io/github/jazzband/django-configurations/coverage.svg?branch=master .. |codecov| image:: https://codecov.io/github/jazzband/django-configurations/coverage.svg?branch=master
:alt: Codecov
:target: https://codecov.io/github/jazzband/django-configurations?branch=master :target: https://codecov.io/github/jazzband/django-configurations?branch=master
:alt: Test coverage status
.. |docs| image:: https://img.shields.io/readthedocs/django-configurations/latest.svg .. |docs| image:: https://img.shields.io/readthedocs/django-configurations/latest.svg
:alt: Documentation status
:target: https://readthedocs.org/projects/django-configurations/ :target: https://readthedocs.org/projects/django-configurations/
:alt: Documentation status
.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-configurations.svg .. |python-support| image:: https://img.shields.io/pypi/pyversions/django-configurations.svg
:target: https://pypi.python.org/pypi/django-configurations :target: https://pypi.python.org/pypi/django-configurations
:alt: Python versions :alt: Supported Python versions
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg
:alt: Jazzband .. |django-support| image:: https://img.shields.io/pypi/djversions/django-configurations
:target: https://jazzband.co/ :target: https://pypi.org/project/django-configurations
:alt: Supported Django versions
.. _documentation: https://django-configurations.readthedocs.io/en/latest/ .. _documentation: https://django-configurations.readthedocs.io/en/latest/
Quickstart Quickstart
@ -37,13 +47,13 @@ Install django-configurations:
.. code-block:: console .. code-block:: console
pip install django-configurations $ python -m pip install django-configurations
or, alternatively, if you want to use URL-based values: or, alternatively, if you want to use URL-based values:
.. code-block:: console .. 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 Then subclass the included ``configurations.Configuration`` class in your
project's **settings.py** or any other module you're using to store the project's **settings.py** or any other module you're using to store the
@ -63,14 +73,14 @@ you just created, e.g. in bash:
.. code-block:: console .. code-block:: console
export DJANGO_CONFIGURATION=Dev $ export DJANGO_CONFIGURATION=Dev
and the ``DJANGO_SETTINGS_MODULE`` environment variable to the module and the ``DJANGO_SETTINGS_MODULE`` environment variable to the module
import path as usual, e.g. in bash: import path as usual, e.g. in bash:
.. code-block:: console .. code-block:: console
export DJANGO_SETTINGS_MODULE=mysite.settings $ export DJANGO_SETTINGS_MODULE=mysite.settings
*Alternatively* supply the ``--configuration`` option when using Django *Alternatively* supply the ``--configuration`` option when using Django
management commands along the lines of Django's default ``--settings`` management commands along the lines of Django's default ``--settings``
@ -78,7 +88,7 @@ command line option, e.g.
.. code-block:: console .. 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 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 **manage.py**, **wsgi.py** or **asgi.py** script to use django-configurations's versions
@ -127,7 +137,7 @@ Or if you are not serving your app via WSGI but ASGI instead, you need to modify
import os import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 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 from configurations.asgi import get_asgi_application

View file

@ -31,7 +31,6 @@ class ConfigurationBase(type):
if parents: if parents:
for base in bases[::-1]: for base in bases[::-1]:
settings_vars.update(uppercase_attributes(base)) settings_vars.update(uppercase_attributes(base))
attrs = dict(settings_vars, **attrs)
deprecated_settings = { deprecated_settings = {
# DEFAULT_HASHING_ALGORITHM is always deprecated, as it's a # DEFAULT_HASHING_ALGORITHM is always deprecated, as it's a
@ -47,20 +46,31 @@ class ConfigurationBase(type):
# suppressed, as downstream users are expected to make a decision. # 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 # https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys
"DEFAULT_AUTO_FIELD", "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_DAYS is deprecated in favor of
# PASSWORD_RESET_TIMEOUT in Django 3.1 # PASSWORD_RESET_TIMEOUT in Django 3.1
# https://github.com/django/django/commit/226ebb17290b604ef29e82fb5c1fbac3594ac163#diff-ec2bed07bb264cb95a80f08d71a47c06R163-R170 # https://github.com/django/django/commit/226ebb17290b604ef29e82fb5c1fbac3594ac163#diff-ec2bed07bb264cb95a80f08d71a47c06R163-R170
if "PASSWORD_RESET_TIMEOUT" in attrs: if "PASSWORD_RESET_TIMEOUT" in settings_vars:
deprecated_settings.add("PASSWORD_RESET_TIMEOUT_DAYS") 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: for deprecated_setting in deprecated_settings:
if deprecated_setting in attrs: if deprecated_setting in settings_vars:
del attrs[deprecated_setting] del settings_vars[deprecated_setting]
attrs = {**settings_vars, **attrs}
return super().__new__(cls, name, bases, attrs) return super().__new__(cls, name, bases, attrs)
def __repr__(self): def __repr__(self):
return "<Configuration '{0}.{1}'>".format(self.__module__, return "<Configuration '{}.{}'>".format(self.__module__,
self.__name__) self.__name__)
@ -97,10 +107,10 @@ class Configuration(metaclass=ConfigurationBase):
environment variables from a .env file located in the project root environment variables from a .env file located in the project root
or provided directory. 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 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) dotenv = getattr(cls, 'DOTENV', None)
# if DOTENV is falsy we want to disable it # 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 # now check if we can access the file since we know we really want to
try: try:
with open(dotenv, 'r') as f: with open(dotenv) as f:
content = f.read() content = f.read()
except OSError as e: except OSError as e:
raise ImproperlyConfigured("Couldn't read .env file " raise ImproperlyConfigured("Couldn't read .env file "

View file

@ -1,4 +1,4 @@
import imp from importlib.machinery import PathFinder
import logging import logging
import os import os
import sys import sys
@ -46,12 +46,12 @@ def install(check_options=False):
return parser return parser
base.BaseCommand.create_parser = create_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) sys.meta_path.insert(0, importer)
installed = True installed = True
class ConfigurationImporter: class ConfigurationFinder(PathFinder):
modvar = SETTINGS_ENVIRONMENT_VARIABLE modvar = SETTINGS_ENVIRONMENT_VARIABLE
namevar = CONFIGURATION_ENVIRONMENT_VARIABLE namevar = CONFIGURATION_ENVIRONMENT_VARIABLE
error_msg = ("Configuration cannot be imported, " error_msg = ("Configuration cannot be imported, "
@ -70,7 +70,7 @@ class ConfigurationImporter:
self.announce() self.announce()
def __repr__(self): def __repr__(self):
return "<ConfigurationImporter for '{0}.{1}'>".format(self.module, return "<ConfigurationFinder for '{}.{}'>".format(self.module,
self.name) self.name)
@property @property
@ -121,58 +121,60 @@ class ConfigurationImporter:
if (self.argv[1] == 'runserver' if (self.argv[1] == 'runserver'
and os.environ.get('RUN_MAIN') == 'true'): and os.environ.get('RUN_MAIN') == 'true'):
message = ("django-configurations version {0}, using " message = ("django-configurations version {}, using "
"configuration {1}".format(__version__ or "", "configuration {}".format(__version__ or "",
self.name)) self.name))
self.logger.debug(stylize(message)) 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: if fullname is not None and fullname == self.module:
module = fullname.rsplit('.', 1)[-1] spec = super().find_spec(fullname, path, target)
return ConfigurationLoader(self.name, if spec is not None:
imp.find_module(module, path)) wrap_loader(spec.loader, self.name)
return None return spec
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: else:
mod = imp.load_module(fullname, *self.location) return None
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)
setattr(mod, 'CONFIGURATION', '{0}.{1}'.format(fullname, def wrap_loader(loader, class_name):
self.name)) class ConfigurationLoader(loader.__class__):
cls.post_setup() def exec_module(self, module):
super().exec_module(module)
except Exception as err: mod = module
reraise(err, "Couldn't setup configuration '{0}'".format(cls_path))
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: try:
module_path, class_name = dotted_path.rsplit('.', 1) module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError: except ValueError:
raise ImproperlyConfigured("{0}{1} doesn't look like " raise ImproperlyConfigured("{}{} doesn't look like "
"a module path".format(error_prefix, "a module path".format(error_prefix,
dotted_path)) dotted_path))
try: try:
module = import_module(module_path) module = import_module(module_path)
except ImportError as err: except ImportError as err:
msg = '{0}Error importing module {1}: "{2}"'.format(error_prefix, msg = '{}Error importing module {}: "{}"'.format(error_prefix,
module_path, module_path,
err) err)
raise ImproperlyConfigured(msg).with_traceback(sys.exc_info()[2]) raise ImproperlyConfigured(msg).with_traceback(sys.exc_info()[2])
try: try:
attr = getattr(module, class_name) attr = getattr(module, class_name)
except AttributeError: except AttributeError:
raise ImproperlyConfigured('{0}Module "{1}" does not define a ' raise ImproperlyConfigured('{}Module "{}" does not define a '
'"{2}" attribute/class'.format(error_prefix, '"{}" attribute/class'.format(error_prefix,
module_path, module_path,
class_name)) class_name))
return attr return attr
@ -61,7 +61,7 @@ def reraise(exc, prefix=None, suffix=None):
suffix = '' suffix = ''
elif not (suffix.startswith('(') and suffix.endswith(')')): elif not (suffix.startswith('(') and suffix.endswith(')')):
suffix = '(' + suffix + ')' 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 raise exc

View file

@ -92,7 +92,7 @@ class Value:
else: else:
environ_name = name.upper() environ_name = name.upper()
if self.environ_prefix: 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 return environ_name
def setup(self, name): def setup(self, name):
@ -102,8 +102,8 @@ class Value:
if full_environ_name in os.environ: if full_environ_name in os.environ:
value = self.to_python(os.environ[full_environ_name]) value = self.to_python(os.environ[full_environ_name])
elif self.environ_required: elif self.environ_required:
raise ValueError('Value {0!r} is required to be set as the ' raise ValueError('Value {!r} is required to be set as the '
'environment variable {1!r}' 'environment variable {!r}'
.format(name, full_environ_name)) .format(name, full_environ_name))
self.value = value self.value = value
return value return value
@ -112,7 +112,7 @@ class Value:
""" """
Convert the given value of a environment variable into an Convert the given value of a environment variable into an
appropriate Python representation of the value. appropriate Python representation of the value.
This should be overriden when subclassing. This should be overridden when subclassing.
""" """
return value return value
@ -128,7 +128,7 @@ class BooleanValue(Value):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.default not in (True, False): 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)) 'boolean value'.format(self.default))
def to_python(self, value): def to_python(self, value):
@ -139,7 +139,7 @@ class BooleanValue(Value):
return False return False
else: else:
raise ValueError('Cannot interpret ' raise ValueError('Cannot interpret '
'boolean value {0!r}'.format(value)) 'boolean value {!r}'.format(value))
class CastingMixin: class CastingMixin:
@ -152,12 +152,12 @@ class CastingMixin:
try: try:
self._caster = import_string(self.caster) self._caster = import_string(self.caster)
except ImportError as err: 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 raise ImproperlyConfigured(msg) from err
elif callable(self.caster): elif callable(self.caster):
self._caster = self.caster self._caster = self.caster
else: else:
error = 'Cannot use caster of {0} ({1!r})'.format(self, error = 'Cannot use caster of {} ({!r})'.format(self,
self.caster) self.caster)
raise ValueError(error) raise ValueError(error)
try: try:
@ -345,13 +345,13 @@ class ValidationMixin:
try: try:
self._validator = import_string(self.validator) self._validator = import_string(self.validator)
except ImportError as err: 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 raise ImproperlyConfigured(msg) from err
elif callable(self.validator): elif callable(self.validator):
self._validator = self.validator self._validator = self.validator
else: else:
raise ValueError('Cannot use validator of ' raise ValueError('Cannot use validator of '
'{0} ({1!r})'.format(self, self.validator)) '{} ({!r})'.format(self, self.validator))
if self.default: if self.default:
self.to_python(self.default) self.to_python(self.default)
@ -397,7 +397,7 @@ class PathValue(Value):
value = super().setup(name) value = super().setup(name)
value = os.path.expanduser(value) value = os.path.expanduser(value)
if self.check_exists and not os.path.exists(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) return os.path.abspath(value)
@ -414,7 +414,7 @@ class SecretValue(Value):
def setup(self, name): def setup(self, name):
value = super().setup(name) value = super().setup(name)
if not value: 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 return value

View file

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

View file

@ -3,9 +3,50 @@
Changelog 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)
^^^^^^^^^^^^^^^^^^^
- Add compatibility with Django 4.0
- Fix regression where settings receiving a default were ignored. #323 #327
v2.3.1 (2021-11-08) v2.3.1 (2021-11-08)
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^

View file

@ -2,7 +2,7 @@ import configurations
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'django-configurations' 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' author = 'Jannis Leidel and other contributors'
release = configurations.__version__ release = configurations.__version__
@ -28,7 +28,7 @@ intersphinx_mapping = {
} }
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
html_theme = 'sphinx_rtd_theme' html_theme = 'furo'
# -- Options for Epub output --------------------------------------------------- # -- Options for Epub output ---------------------------------------------------
epub_title = project epub_title = project

View file

@ -74,7 +74,7 @@ Example:
.. code-block:: console .. code-block:: console
$ tree mysite_env/ $ tree --noreport mysite_env/
mysite_env/ mysite_env/
├── DJANGO_SETTINGS_MODULE ├── DJANGO_SETTINGS_MODULE
├── DJANGO_DEBUG ├── DJANGO_DEBUG
@ -82,10 +82,8 @@ Example:
├── DJANGO_CACHE_URL ├── DJANGO_CACHE_URL
└── PYTHONSTARTUP └── PYTHONSTARTUP
0 directories, 3 files
$ cat mysite_env/DJANGO_CACHE_URL $ cat mysite_env/DJANGO_CACHE_URL
redis://user@host:port/1 redis://user@host:port/1
$
Then, to enable the ``mysite_env`` environment variables, simply use the Then, to enable the ``mysite_env`` environment variables, simply use the
``envdir`` command line tool as a prefix for your program, e.g.: ``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 .. 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: Or Django 1.8:
.. code-block:: console .. 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`` Now you have a default Django 1.8.x project in the ``mysite``
directory that uses django-configurations. 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 As always your mileage may vary, so please don't hesitate to send feature
requests and bug reports: requests and bug reports:
https://github.com/jazzband/django-configurations/issues - https://github.com/jazzband/django-configurations/issues
Thanks! Thanks!

View file

@ -3,7 +3,7 @@ Usage patterns
There are various configuration patterns that can be implemented with There are various configuration patterns that can be implemented with
django-configurations. The most common pattern is to have a base class 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. used in, e.g. in production, staging and development.
Server specific settings Server specific settings
@ -31,9 +31,9 @@ it should be ``Prod``. In Bash that would be:
.. code-block:: console .. code-block:: console
export DJANGO_SETTINGS_MODULE=mysite.settings $ export DJANGO_SETTINGS_MODULE=mysite.settings
export DJANGO_CONFIGURATION=Prod $ export DJANGO_CONFIGURATION=Prod
python manage.py runserver $ python -m manage runserver
Alternatively you can use the ``--configuration`` option when using Django Alternatively you can use the ``--configuration`` option when using Django
management commands along the lines of Django's default ``--settings`` management commands along the lines of Django's default ``--settings``
@ -41,7 +41,7 @@ command line option, e.g.
.. code-block:: console .. code-block:: console
python manage.py runserver --settings=mysite.settings --configuration=Prod $ python -m manage runserver --settings=mysite.settings --configuration=Prod
Property settings Property settings
----------------- -----------------

View file

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

View file

@ -86,9 +86,11 @@ prefixed with ``DJANGO_``. E.g.:
django-configurations will try to read the ``DJANGO_ROOT_URLCONF`` environment django-configurations will try to read the ``DJANGO_ROOT_URLCONF`` environment
variable when deciding which value the ``ROOT_URLCONF`` setting should have. variable when deciding which value the ``ROOT_URLCONF`` setting should have.
When you run the web server simply specify that environment variable 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 If the environment variable can't be found it'll use the default
``'mysite.urls'``. ``'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 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 directly converted to its final value for use outside of the configuration
context:: context:
.. code-block:: pycon
>>> type(values.Value([])) >>> type(values.Value([]))
<class 'configurations.values.Value'> <class 'configurations.values.Value'>
@ -283,17 +287,21 @@ Type values
MONTY_PYTHONS = ListValue(['John Cleese', 'Eric Idle'], MONTY_PYTHONS = ListValue(['John Cleese', 'Eric Idle'],
converter=check_monty_python) 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:: Use a custom separator::
EMERGENCY_EMAILS = ListValue(['admin@mysite.net'], 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 .. class:: TupleValue

View file

@ -14,6 +14,9 @@ setup(
use_scm_version={"version_scheme": "post-release", "local_scheme": "dirty-tag"}, use_scm_version={"version_scheme": "post-release", "local_scheme": "dirty-tag"},
setup_requires=["setuptools_scm"], setup_requires=["setuptools_scm"],
url='https://django-configurations.readthedocs.io/', url='https://django-configurations.readthedocs.io/',
project_urls={
'Source': 'https://github.com/jazzband/django-configurations',
},
license='BSD', license='BSD',
description="A helper for organizing Django settings.", description="A helper for organizing Django settings.",
long_description=read('README.rst'), long_description=read('README.rst'),
@ -27,10 +30,9 @@ setup(
], ],
}, },
install_requires=[ install_requires=[
'django>=2.2', 'django>=3.2',
'importlib-metadata;python_version<"3.8"',
], ],
python_requires='>=3.6, <4.0', python_requires='>=3.9, <4.0',
extras_require={ extras_require={
'cache': ['django-cache-url'], 'cache': ['django-cache-url'],
'database': ['dj-database-url'], 'database': ['dj-database-url'],
@ -46,19 +48,22 @@ setup(
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Framework :: Django', 'Framework :: Django',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.1',
'Framework :: Django :: 3.2', 'Framework :: Django :: 3.2',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Framework :: Django :: 5.1',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', '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', 'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Utilities', 'Topic :: Utilities',
], ],

View file

@ -6,3 +6,6 @@ class DotEnvConfiguration(Configuration):
DOTENV = 'test_project/.env' DOTENV = 'test_project/.env'
DOTENV_VALUE = values.Value() 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 os
import uuid import uuid
import django
from configurations import Configuration, pristinemethod from configurations import Configuration, pristinemethod
from configurations.values import BooleanValue
class Test(Configuration): class Test(Configuration):
@ -11,8 +9,6 @@ class Test(Configuration):
os.path.join(os.path.dirname( os.path.join(os.path.dirname(
os.path.abspath(__file__)), os.pardir)) os.path.abspath(__file__)), os.pardir))
ENV_LOADED = BooleanValue(False)
DEBUG = True DEBUG = True
SITE_ID = 1 SITE_ID = 1
@ -36,9 +32,6 @@ class Test(Configuration):
ROOT_URLCONF = 'tests.urls' ROOT_URLCONF = 'tests.urls'
if django.VERSION[:2] < (1, 6):
TEST_RUNNER = 'discover_runner.DiscoverRunner'
@property @property
def ALLOWED_HOSTS(self): def ALLOWED_HOSTS(self):
allowed_hosts = super().ALLOWED_HOSTS[:] allowed_hosts = super().ALLOWED_HOSTS[:]
@ -71,3 +64,7 @@ class Test(Configuration):
@classmethod @classmethod
def post_setup(cls): def post_setup(cls):
cls.POST_SETUP_TEST_SETTING = 7 cls.POST_SETUP_TEST_SETTING = 7
class TestWithDefaultSetExplicitely(Test):
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View file

@ -11,4 +11,5 @@ class DotEnvLoadingTests(TestCase):
def test_env_loaded(self): def test_env_loaded(self):
from tests.settings import dot_env from tests.settings import dot_env
self.assertEqual(dot_env.DOTENV_VALUE, 'is set') 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) 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 unittest.mock import patch
from configurations.importer import ConfigurationImporter from configurations.importer import ConfigurationFinder
ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
TEST_PROJECT_DIR = os.path.join(ROOT_DIR, 'test_project') 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') @patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test')
def test_empty_module_var(self): def test_empty_module_var(self):
self.assertRaises(ImproperlyConfigured, ConfigurationImporter) with self.assertRaises(ImproperlyConfigured):
ConfigurationFinder()
@patch.dict(os.environ, clear=True, @patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main') DJANGO_SETTINGS_MODULE='tests.settings.main')
def test_empty_class_var(self): def test_empty_class_var(self):
self.assertRaises(ImproperlyConfigured, ConfigurationImporter) with self.assertRaises(ImproperlyConfigured):
ConfigurationFinder()
def test_global_settings(self): def test_global_settings(self):
from configurations.base import Configuration from configurations.base import Configuration
@ -55,6 +57,12 @@ class MainTests(TestCase):
self.assertEqual(repr(Configuration), self.assertEqual(repr(Configuration),
"<Configuration 'configurations.base.Configuration'>") "<Configuration 'configurations.base.Configuration'>")
def test_deprecated_settings_but_set_by_user(self):
from tests.settings.main import TestWithDefaultSetExplicitely
TestWithDefaultSetExplicitely.setup()
self.assertEqual(TestWithDefaultSetExplicitely.DEFAULT_AUTO_FIELD,
"django.db.models.BigAutoField")
def test_repr(self): def test_repr(self):
from tests.settings.main import Test from tests.settings.main import Test
self.assertEqual(repr(Test), self.assertEqual(repr(Test),
@ -64,21 +72,21 @@ class MainTests(TestCase):
DJANGO_SETTINGS_MODULE='tests.settings.main', DJANGO_SETTINGS_MODULE='tests.settings.main',
DJANGO_CONFIGURATION='Test') DJANGO_CONFIGURATION='Test')
def test_initialization(self): def test_initialization(self):
importer = ConfigurationImporter() finder = ConfigurationFinder()
self.assertEqual(importer.module, 'tests.settings.main') self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(importer.name, 'Test') self.assertEqual(finder.name, 'Test')
self.assertEqual( self.assertEqual(
repr(importer), repr(finder),
"<ConfigurationImporter for 'tests.settings.main.Test'>") "<ConfigurationFinder for 'tests.settings.main.Test'>")
@patch.dict(os.environ, clear=True, @patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.inheritance', DJANGO_SETTINGS_MODULE='tests.settings.inheritance',
DJANGO_CONFIGURATION='Inheritance') DJANGO_CONFIGURATION='Inheritance')
def test_initialization_inheritance(self): def test_initialization_inheritance(self):
importer = ConfigurationImporter() finder = ConfigurationFinder()
self.assertEqual(importer.module, self.assertEqual(finder.module,
'tests.settings.inheritance') 'tests.settings.inheritance')
self.assertEqual(importer.name, 'Inheritance') self.assertEqual(finder.name, 'Inheritance')
@patch.dict(os.environ, clear=True, @patch.dict(os.environ, clear=True,
DJANGO_SETTINGS_MODULE='tests.settings.main', DJANGO_SETTINGS_MODULE='tests.settings.main',
@ -87,12 +95,12 @@ class MainTests(TestCase):
'--settings=tests.settings.main', '--settings=tests.settings.main',
'--configuration=Test']) '--configuration=Test'])
def test_configuration_option(self): def test_configuration_option(self):
importer = ConfigurationImporter(check_options=False) finder = ConfigurationFinder(check_options=False)
self.assertEqual(importer.module, 'tests.settings.main') self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(importer.name, 'NonExisting') self.assertEqual(finder.name, 'NonExisting')
importer = ConfigurationImporter(check_options=True) finder = ConfigurationFinder(check_options=True)
self.assertEqual(importer.module, 'tests.settings.main') self.assertEqual(finder.module, 'tests.settings.main')
self.assertEqual(importer.name, 'Test') self.assertEqual(finder.name, 'Test')
def test_configuration_argument_in_cli(self): def test_configuration_argument_in_cli(self):
""" """

View file

@ -2,6 +2,7 @@ import decimal
import os import os
from contextlib import contextmanager from contextlib import contextmanager
from django import VERSION as DJANGO_VERSION
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -33,7 +34,7 @@ class ValueTests(TestCase):
def test_value_with_default(self): def test_value_with_default(self):
value = Value('default', environ=False) value = Value('default', environ=False)
self.assertEqual(type(value), type('default')) self.assertEqual(type(value), str)
self.assertEqual(value, 'default') self.assertEqual(value, 'default')
self.assertEqual(str(value), 'default') self.assertEqual(str(value), 'default')
@ -43,17 +44,17 @@ class ValueTests(TestCase):
with env(DJANGO_TEST='override'): with env(DJANGO_TEST='override'):
self.assertEqual(value.setup('TEST'), 'default') self.assertEqual(value.setup('TEST'), 'default')
value = Value(environ_name='TEST') value = Value(environ_name='TEST')
self.assertEqual(type(value), type('override')) self.assertEqual(type(value), str)
self.assertEqual(value, 'override') self.assertEqual(value, 'override')
self.assertEqual(str(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('%s' % value, 'override')
value = Value(environ_name='TEST', late_binding=True) value = Value(environ_name='TEST', late_binding=True)
self.assertEqual(type(value), Value) self.assertEqual(type(value), Value)
self.assertEqual(value.value, 'override') self.assertEqual(value.value, 'override')
self.assertEqual(str(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('%s' % value, 'override')
self.assertEqual(repr(value), repr('override')) self.assertEqual(repr(value), repr('override'))
@ -372,16 +373,23 @@ class ValueTests(TestCase):
value = DatabaseURLValue() value = DatabaseURLValue()
self.assertEqual(value.default, {}) self.assertEqual(value.default, {})
with env(DATABASE_URL='sqlite://'): with env(DATABASE_URL='sqlite://'):
self.assertEqual(value.setup('DATABASE_URL'), { settings_value = value.setup('DATABASE_URL')
'default': { # 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, 'CONN_MAX_AGE': 0,
'DISABLE_SERVER_SIDE_CURSORS': False,
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'HOST': '', 'HOST': '',
'NAME': ':memory:', 'NAME': ':memory:',
'PASSWORD': '', 'PASSWORD': '',
'PORT': '', 'PORT': '',
'USER': '', 'USER': '',
}}) },
settings_value['default']
)
def test_database_url_additional_args(self): def test_database_url_additional_args(self):
@ -411,6 +419,7 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': 'password', 'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com', 'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587, 'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False, 'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True}) 'EMAIL_USE_TLS': True})
with env(EMAIL_URL='console://'): with env(EMAIL_URL='console://'):
@ -421,6 +430,7 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': None, 'EMAIL_HOST_PASSWORD': None,
'EMAIL_HOST_USER': None, 'EMAIL_HOST_USER': None,
'EMAIL_PORT': None, 'EMAIL_PORT': None,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False, 'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': False}) 'EMAIL_USE_TLS': False})
with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:wrong'): # noqa: E501 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): def test_cache_url_value(self):
cache_setting = { cache_setting = {
'default': { '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', 'LOCATION': 'redis://host:6379/1',
} }
} }
@ -503,6 +513,7 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': 'password', 'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com', 'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587, 'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False, 'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True 'EMAIL_USE_TLS': True
}) })

33
tox.ini
View file

@ -3,22 +3,22 @@ skipsdist = true
usedevelop = true usedevelop = true
minversion = 1.8 minversion = 1.8
envlist = envlist =
py36-checkqa py311-checkqa
docs docs
py{36,37,py36,py37}-dj{22,31,32} py{39}-dj{32,41,42}
py{38,39,py38}-dj{22,31,32,40,main} py{310,py310}-dj{32,41,42,50,main}
py{310}-dj{32,40,main} py{311}-dj{41,42,50,51,main}
py{312}-dj{50,51,main}
py{313}-dj{50,51,main}
[gh-actions] [gh-actions]
python = python =
3.6: py36,flake8,readme
3.7: py37
3.8: py38
3.9: py39 3.9: py39
3.10: py310 3.10: py310
pypy-3.6: pypy36 3.11: py311,flake8,readme
pypy-3.7: pypy37 3.12: py312
pypy-3.8: pypy38 3.13: py313
pypy-3.10: pypy310
[testenv] [testenv]
usedevelop = true usedevelop = true
@ -27,11 +27,16 @@ setenv =
DJANGO_CONFIGURATION = Test DJANGO_CONFIGURATION = Test
COVERAGE_PROCESS_START = {toxinidir}/setup.cfg COVERAGE_PROCESS_START = {toxinidir}/setup.cfg
deps = deps =
dj22: django~=2.2.17
dj31: django~=3.1.3
dj32: django~=3.2.9 dj32: django~=3.2.9
dj40: django>=4.0b1,<4.1 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 djmain: https://github.com/django/django/archive/main.tar.gz
py312: setuptools
py312: wheel
py313: setuptools
py313: wheel
coverage coverage
coverage_enable_subprocess coverage_enable_subprocess
extras = testing extras = testing
@ -42,7 +47,7 @@ commands =
coverage report -m --skip-covered coverage report -m --skip-covered
coverage xml coverage xml
[testenv:py36-checkqa] [testenv:py311-checkqa]
commands = commands =
flake8 {toxinidir} flake8 {toxinidir}
check-manifest -v check-manifest -v