New Upstream Release - python-whitenoise
Ready changes
Summary
Merged new upstream version: 6.4.0 (was: 6.2.0).
Diff
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..40f4ac7
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1 @@
+This project follows [Django's Code of Conduct](https://www.djangoproject.com/conduct/).
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3ba13e0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
new file mode 100644
index 0000000..2a7f84f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -0,0 +1,10 @@
+name: Feature Request
+description: Request an enhancement or new feature.
+body:
+- type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Please describe your feature request with appropriate detail.
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml
new file mode 100644
index 0000000..26d8ce4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue.yml
@@ -0,0 +1,34 @@
+name: Issue
+description: File an issue
+body:
+- type: input
+ id: python_version
+ attributes:
+ label: Python Version
+ description: Which version of Python were you using?
+ placeholder: 3.9.0
+ validations:
+ required: false
+- type: input
+ id: django_version
+ attributes:
+ label: Django Version
+ description: Which version of Django were you using?
+ placeholder: 3.2.0
+ validations:
+ required: false
+- type: input
+ id: package_version
+ attributes:
+ label: Package Version
+ description: Which version of this package were you using? If not the latest version, please check this issue has not since been resolved.
+ placeholder: 1.0.0
+ validations:
+ required: false
+- type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Please describe your issue.
+ validations:
+ required: true
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 0000000..205ab9a
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1 @@
+Please report security issues directly over email to me@adamj.eu
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..e5903e0
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+- package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 36d3bcb..ca9883e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,7 +3,7 @@ name: CI
on:
push:
branches:
- - master
+ - main
pull_request:
concurrency:
@@ -18,18 +18,19 @@ jobs:
strategy:
matrix:
os:
- - ubuntu-20.04
+ - ubuntu-22.04
- windows-2022
python-version:
- - "3.7"
- - "3.8"
- - "3.9"
- - "3.10"
+ - '3.7'
+ - '3.8'
+ - '3.9'
+ - '3.10'
+ - '3.11'
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- - uses: actions/setup-python@v2
+ - uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: pip
@@ -38,34 +39,34 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
- python -m pip install --upgrade tox tox-py
+ python -m pip install --upgrade 'tox>=4.0.0rc3'
- name: Run tox targets for ${{ matrix.python-version }}
- run: tox --py current
+ run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
- name: Upload coverage data
if: matrix.os != 'windows-2022'
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: coverage-data
path: '.coverage.*'
coverage:
name: Coverage
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
needs: tests
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- - uses: actions/setup-python@v2
+ - uses: actions/setup-python@v4
with:
- python-version: '3.10'
+ python-version: '3.11'
- name: Install dependencies
run: python -m pip install --upgrade coverage[toml]
- name: Download data
- uses: actions/download-artifact@v2
+ uses: actions/download-artifact@v3
with:
name: coverage-data
@@ -77,7 +78,7 @@ jobs:
- name: Upload HTML report
if: ${{ failure() }}
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: html-report
path: htmlcov
diff --git a/.gitignore b/.gitignore
index d971bf8..db70699 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,8 @@
-*.py[co]
-__pycache__
-/MANIFEST
-/.tox
+*.egg-info/
+*.pyc
/.coverage
/.coverage.*
-/htmlcov
-/docs/_build
-/dist
-/src/*.egg-info
+/.tox
+/build/
+/dist/
+/docs/_build/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 91e8e93..5e20c84 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,9 +1,9 @@
default_language_version:
- python: python3.10
+ python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.1.0
+ rev: v4.4.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@@ -13,27 +13,43 @@ repos:
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
+- repo: https://github.com/tox-dev/pyproject-fmt
+ rev: 0.9.1
+ hooks:
+ - id: pyproject-fmt
+- repo: https://github.com/asottile/setup-cfg-fmt
+ rev: v2.2.0
+ hooks:
+ - id: setup-cfg-fmt
+ args:
+ - --include-version-classifiers
- repo: https://github.com/asottile/pyupgrade
- rev: v2.31.0
+ rev: v3.3.1
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/psf/black
- rev: 22.1.0
+ rev: 23.1.0
hooks:
- id: black
-- repo: https://github.com/asottile/blacken-docs
- rev: v1.12.1
+- repo: https://github.com/adamchainz/blacken-docs
+ rev: 1.13.0
hooks:
- id: blacken-docs
additional_dependencies:
- - black==22.1.0
-- repo: https://github.com/pycqa/isort
- rev: 5.10.1
+ - black==23.1.0
+- repo: https://github.com/asottile/reorder_python_imports
+ rev: v3.9.0
hooks:
- - id: isort
+ - id: reorder-python-imports
+ args:
+ - --py37-plus
+ - --application-directories
+ - .:example:src
+ - --add-import
+ - 'from __future__ import annotations'
- repo: https://github.com/PyCQA/flake8
- rev: 4.0.1
+ rev: 6.0.0
hooks:
- id: flake8
additional_dependencies:
@@ -41,8 +57,3 @@ repos:
- flake8-comprehensions
- flake8-tidy-imports
- flake8-typing-imports
-- repo: https://github.com/mgedmin/check-manifest
- rev: "0.47"
- hooks:
- - id: check-manifest
- args: [--no-build-isolation]
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index f497b49..c05795f 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -4,12 +4,13 @@
version: 2
build:
- os: ubuntu-20.04
+ os: ubuntu-22.04
tools:
- python: "3.10"
+ python: "3.11"
sphinx:
configuration: docs/conf.py
+ fail_on_warning: true
formats: all
diff --git a/MANIFEST.in b/MANIFEST.in
index a0cc8c9..a57e5db 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,14 +1,7 @@
-global-exclude *.py[cod]
-prune __pycache__
prune docs
-prune requirements
prune scripts
-prune tests
-exclude .editorconfig
-exclude .pre-commit-config.yaml
exclude .readthedocs.yaml
exclude CHANGELOG.rst
-exclude tox.ini
include LICENSE
include pyproject.toml
include README.rst
diff --git a/README.rst b/README.rst
index 57464c4..e233c11 100644
--- a/README.rst
+++ b/README.rst
@@ -1,25 +1,25 @@
+==========
WhiteNoise
==========
-.. image:: https://img.shields.io/travis/evansd/whitenoise.svg
- :target: https://travis-ci.org/evansd/whitenoise
- :alt: Build Status (Linux)
+.. image:: https://img.shields.io/readthedocs/whitenoise?style=for-the-badge
+ :target: https://whitenoise.evans.io/en/latest/
+
+.. image:: https://img.shields.io/github/actions/workflow/status/evansd/whitenoise/main.yml?branch=master&style=for-the-badge
+ :target: https://github.com/evansd/whitenoise/actions?workflow=CI
-.. image:: https://img.shields.io/appveyor/ci/evansd/whitenoise.svg
- :target: https://ci.appveyor.com/project/evansd/whitenoise
- :alt: Build Status (Windows)
+.. image:: https://img.shields.io/badge/Coverage-96%25-success?style=for-the-badge
+ :target: https://github.com/evansd/whitenoise/actions?workflow=CI
-.. image:: https://img.shields.io/pypi/v/whitenoise.svg
- :target: https://pypi.python.org/pypi/whitenoise
- :alt: Latest PyPI version
+.. image:: https://img.shields.io/pypi/v/whitenoise.svg?style=for-the-badge
+ :target: https://pypi.org/project/whitenoise/
-.. image:: https://img.shields.io/pypi/dm/whitenoise.svg
- :target: https://pypistats.org/packages/whitenoise
- :alt: Monthly PyPI downloads
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge
+ :target: https://github.com/psf/black
-.. image:: https://img.shields.io/github/stars/evansd/whitenoise.svg?style=social&label=Star
- :target: https://github.com/evansd/whitenoise
- :alt: GitHub project
+.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge
+ :target: https://github.com/pre-commit/pre-commit
+ :alt: pre-commit
**Radically simplified static file serving for Python web apps**
diff --git a/debian/changelog b/debian/changelog
index 9c6d723..afc5df3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+python-whitenoise (6.4.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sat, 20 May 2023 14:10:40 -0000
+
python-whitenoise (6.0.0-3) unstable; urgency=medium
[ Debian Janitor ]
diff --git a/docs/base.rst b/docs/base.rst
index ea3be04..b0b272a 100644
--- a/docs/base.rst
+++ b/docs/base.rst
@@ -185,6 +185,7 @@ sub-classing WhiteNoise and setting the attributes directly.
long enough that, if you're running WhiteNoise behind a CDN, the CDN will still take
the majority of the strain during times of heavy load.
+ Set to ``None`` to disable setting any ``Cache-Control`` header on non-versioned files.
.. attribute:: index_file
@@ -193,7 +194,6 @@ sub-classing WhiteNoise and setting the attributes directly.
If ``True`` enable :ref:`index file serving <index_files>`. If set to a non-empty
string, enable index files and use that string as the index file name.
-
.. attribute:: mimetypes
:default: ``None``
@@ -206,7 +206,7 @@ sub-classing WhiteNoise and setting the attributes directly.
Note that WhiteNoise ships with its own default set of mimetypes and does
not use the system-supplied ones (e.g. ``/etc/mime.types``). This ensures
that it behaves consistently regardless of the environment in which it's
- run. View the defaults in the :file:`media_types.py
+ run. View the defaults in the :ghfile:`media_types.py
<whitenoise/media_types.py>` file.
In addition to file extensions, mimetypes can be specified by supplying the entire
diff --git a/docs/changelog.rst b/docs/changelog.rst
index fff2086..5bd9c90 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,8 +1,47 @@
+=========
Changelog
=========
-6.0.0
------
+6.4.0 (2023-02-25)
+------------------
+
+* Support Django 4.2.
+
+* Remove further support for byte strings from the ``root`` and ``prefix`` arguments to ``WhiteNoise``, and Django’s ``STATIC_ROOT`` setting.
+ Like in the previous release, this seems to be a remnant of Python 2 support.
+ Again, this change may be backwards incompatible for a small number of projects, but it’s unlikely.
+ Django does not support ``STATIC_ROOT`` being a byte string.
+
+6.3.0 (2023-01-03)
+------------------
+
+* Add some video file extensions to be ignored during compression.
+ Since such files are already heavily compressed, further compression rarely helps.
+
+ Thanks to Jon Ribbens in `PR #431 <https://github.com/evansd/whitenoise/pull/431>`__.
+
+* Remove the behaviour of decoding byte strings passed for settings that take strings.
+ This seemed to be left around from supporting Python 2.
+ This change may be backwards incompatible for a small number of projects.
+
+* Document “hidden” feature of setting ``max_age`` to ``None`` to disable the ``Cache-Control`` header.
+
+* Drop support for working as old-style Django middleware, as support was `removed in Django 2.0 <https://docs.djangoproject.com/en/dev/releases/2.0/#features-removed-in-2-0>`__.
+
+6.2.0 (2022-06-05)
+------------------
+
+* Support Python 3.11.
+
+* Support Django 4.1.
+
+6.1.0 (2022-05-10)
+------------------
+
+* Drop support for Django 2.2, 3.0, and 3.1.
+
+6.0.0 (2022-02-10)
+------------------
* Drop support for Python 3.5 and 3.6.
@@ -16,16 +55,12 @@ Changelog
- ``.avif`` files are now served with the ``image/avif`` MIME type.
- - Open Document files with extensions ``.odg``, ``.odp``, ``.ods``, and
- ``.odt`` are now served with their respective
- ``application/vnd.oasis.opendocument.*`` MIME types.
+ - Open Document files with extensions ``.odg``, ``.odp``, ``.ods``, and ``.odt`` are now served with their respective ``application/vnd.oasis.opendocument.*`` MIME types.
-* The ``whitenoise.__version__`` attribute has been removed. Use
- ``importlib.metadata.version()`` to check the version of Whitenoise if you
- need to.
+* The ``whitenoise.__version__`` attribute has been removed.
+ Use ``importlib.metadata.version()`` to check the version of Whitenoise if you need to.
-* Requests using the ``Range`` header can no longer read beyond the end of the
- requested range.
+* Requests using the ``Range`` header can no longer read beyond the end of the requested range.
Thanks to Richard Tibbles in `PR #322 <https://github.com/evansd/whitenoise/pull/322>`__.
@@ -36,36 +71,32 @@ Changelog
5.3.0 (2021-07-16)
------------------
-* Gracefully handle unparsable If-Modified-Since headers (thanks
- `@danielegozzi <https://github.com/danielegozzi>`_).
+* Gracefully handle unparsable ``If-Modified-Since`` headers (thanks `@danielegozzi <https://github.com/danielegozzi>`_).
+
* Test against Django 3.2 (thanks `@jhnbkr <https://github.com/jhnbkr>`_).
-* Add mimetype for Markdown (``.md``) files (thanks `@bz2
- <https://github.com/bz2>`_).
-* Various documentation improvements (thanks `@PeterJCLaw
- <https://github.com/PeterJCLaw>`_ and `@AliRn76
- <https://github.com/AliRn76>`_).
+
+* Add mimetype for Markdown (``.md``) files (thanks `@bz2 <https://github.com/bz2>`_).
+
+* Various documentation improvements (thanks `@PeterJCLaw <https://github.com/PeterJCLaw>`_ and `@AliRn76 <https://github.com/AliRn76>`_).
5.2.0 (2020-08-04)
------------------
-* Add support for `relative STATIC_URLs <https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-STATIC_URL>`_
- in settings, as allowed in Django 3.1.
-* Add mimetype for ``.mjs`` (JavaScript module) files and use recommended
- ``text/javascript`` mimetype for ``.js`` files (thanks `@hanswilw <https://github.com/hanswilw>`_).
+* Add support for `relative STATIC_URLs <https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-STATIC_URL>`_ in settings, as allowed in Django 3.1.
+
+* Add mimetype for ``.mjs`` (JavaScript module) files and use recommended ``text/javascript`` mimetype for ``.js`` files (thanks `@hanswilw <https://github.com/hanswilw>`_).
+
* Various documentation improvements (thanks `@lukeburden <https://github.com/lukeburden>`_).
5.1.0 (2020-05-20)
------------------
-* Add a :any:`manifest_strict <WHITENOISE_MANIFEST_STRICT>` setting to prevent
- Django throwing errors when missing files are referenced (thanks
- `@MegacoderKim <https://github.com/MegacoderKim>`_).
+* Add a :any:`manifest_strict <WHITENOISE_MANIFEST_STRICT>` setting to prevent Django throwing errors when missing files are referenced (thanks `@MegacoderKim <https://github.com/MegacoderKim>`_).
5.0.1 (2019-12-12)
------------------
-* Fix packaging to indicate only Python 3.5+ compatibiity (thanks `@mdalp
- <https://github.com/mdalp>`_).
+* Fix packaging to indicate only Python 3.5+ compatibiity (thanks `@mdalp <https://github.com/mdalp>`_).
5.0 (2019-12-10)
----------------
@@ -78,60 +109,52 @@ Changelog
Other changes include:
-* Fix incompatibility with Django 3.0 which caused problems with Safari
- (details `here <https://github.com/evansd/whitenoise/issues/240>`_). Thanks
- `@paltman <https://github.com/paltman>`_ and `@giilby
- <https://github.com/giilby>`_ for diagnosing.
+* Fix incompatibility with Django 3.0 which caused problems with Safari (details `here <https://github.com/evansd/whitenoise/issues/240>`_).
+ Thanks `@paltman <https://github.com/paltman>`_ and `@giilby <https://github.com/giilby>`_ for diagnosing.
+
* Lots of improvements to the test suite (including switching to py.test).
- Thanks `@NDevox <https://github.com/ndevox>`_ and `@Djailla
- <https://github.com/djailla>`_.
+ Thanks `@NDevox <https://github.com/ndevox>`_ and `@Djailla <https://github.com/djailla>`_.
4.1.4 (2019-09-24)
------------------
* Make tests more deterministic and easier to run outside of ``tox``.
+
* Fix Fedora packaging `issue <https://github.com/evansd/whitenoise/issues/225>`_.
+
* Use `Black <https://github.com/psf/black>`_ to format all code.
4.1.3 (2019-07-13)
------------------
-* Fix handling of zero-valued mtimes which can occur when running on some
- filesystems (thanks `@twosigmajab <https://github.com/twosigmajab>`_ for
- reporting).
-* Fix potential path traversal attack while running in autorefresh mode on
- Windows (thanks `@phith0n <https://github.com/phith0n>`_ for reporting).
- This is a good time to reiterate that autofresh mode is never intended for
- production use.
+* Fix handling of zero-valued mtimes which can occur when running on some filesystems (thanks `@twosigmajab <https://github.com/twosigmajab>`_ for reporting).
+
+* Fix potential path traversal attack while running in autorefresh mode on Windows (thanks `@phith0n <https://github.com/phith0n>`_ for reporting).
+ This is a good time to reiterate that autofresh mode is never intended for production use.
4.1.2 (2019-11-19)
------------------
-* Add correct MIME type for WebAssembly, which is required for files to be
- executed (thanks `@mdboom <https://github.com/mdboom>`_ ).
-* Stop accessing the FILE_CHARSET Django setting which was almost entirely
- unused and is now deprecated (thanks `@timgraham
- <https://github.com/timgraham>`_).
+* Add correct MIME type for WebAssembly, which is required for files to be executed (thanks `@mdboom <https://github.com/mdboom>`_ ).
+
+* Stop accessing the ``FILE_CHARSET`` Django setting which was almost entirely unused and is now deprecated (thanks `@timgraham <https://github.com/timgraham>`_).
4.1.1 (2018-11-12)
------------------
-* Fix `bug <https://github.com/evansd/whitenoise/issues/202>`_ in ETag
- handling (thanks `@edmorley <https://github.com/edmorley>`_).
-* Documentation fixes (thanks `@jamesbeith <https://github.com/jamesbeith>`_
- and `@mathieusteele <https://github.com/mathieusteele>`_).
+* Fix `bug <https://github.com/evansd/whitenoise/issues/202>`_ in ETag handling (thanks `@edmorley <https://github.com/edmorley>`_).
+
+* Documentation fixes (thanks `@jamesbeith <https://github.com/jamesbeith>`_ and `@mathieusteele <https://github.com/mathieusteele>`_).
4.1 (2018-09-12)
----------------
-* Silenced spurious warning about missing directories when in development (i.e
- "autorefresh") mode.
-* Support supplying paths as `Pathlib
- <https://docs.python.org/3.4/library/pathlib.html>`_ instances, rather than
- just strings (thanks `@browniebroke <https://github.com/browniebroke>`_).
-* Add a new :ref:`CompressedStaticFilesStorage <compression-and-caching>`
- backend to support applying compression without applying Django's hash-versioning
- process.
+* Silenced spurious warning about missing directories when in development (i.e "autorefresh") mode.
+
+* Support supplying paths as `Pathlib <https://docs.python.org/3.4/library/pathlib.html>`_ instances, rather than just strings (thanks `@browniebroke <https://github.com/browniebroke>`_).
+
+* Add a new :ref:`CompressedStaticFilesStorage <compression-and-caching>` backend to support applying compression without applying Django's hash-versioning process.
+
* Documentation improvements.
4.0 (2018-08-10)
@@ -266,35 +289,34 @@ installing WhiteNoise and Brotli together like this:
3.3.1 (2017-09-23)
------------------
-* Fix issue with the immutable file test when running behind a CDN which rewrites
- paths (thanks @lskillen).
+* Fix issue with the immutable file test when running behind a CDN which rewrites paths (thanks @lskillen).
3.3.0 (2017-01-26)
------------------
-* Support the new `immutable <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Revalidation_and_reloading>`_
- Cache-Control header. This gives better caching behaviour for immutable resources than
- simply setting a large max age.
+* Support the new `immutable <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Revalidation_and_reloading>`_ Cache-Control header.
+ This gives better caching behaviour for immutable resources than simply setting a large max age.
3.2.3 (2017-01-04)
------------------
* Gracefully handle invalid byte sequences in URLs.
+
* Gracefully handle filenames which are too long for the filesystem.
+
* Send correct Content-Type for Adobe's ``crossdomain.xml`` files.
3.2.2 (2016-09-26)
------------------
-* Convert any config values supplied as byte strings to text to avoid
- runtime encoding errors when encountering non-ASCII filenames.
+* Convert any config values supplied as byte strings to text to avoid runtime encoding errors when encountering non-ASCII filenames.
3.2.1 (2016-08-09)
------------------
* Handle non-ASCII URLs correctly when using the ``wsgi.py`` integration.
-* Fix exception triggered when a static files "finder" returned a directory
- rather than a file.
+
+* Fix exception triggered when a static files "finder" returned a directory rather than a file.
3.2 (2016-05-27)
----------------
@@ -302,8 +324,10 @@ installing WhiteNoise and Brotli together like this:
* Add support for the new-style middleware classes introduced in Django 1.10.
The same WhiteNoiseMiddleware class can now be used in either the old
``MIDDLEWARE_CLASSES`` list or the new ``MIDDLEWARE`` list.
+
* Fixed a bug where incorrect Content-Type headers were being sent on 304 Not
Modified responses (thanks `@oppianmatt <https://github.com/oppianmatt>`_).
+
* Return Vary and Cache-Control headers on 304 responses, as specified by the
`RFC <https://tools.ietf.org/html/rfc7232#section-4.1>`_.
@@ -313,6 +337,7 @@ installing WhiteNoise and Brotli together like this:
* Add new :any:`WHITENOISE_STATIC_PREFIX` setting to give flexibility in
supporting non-standard deployment configurations e.g. serving the
application somewhere other than the domain root.
+
* Fix bytes/unicode bug when running with Django 1.10 on Python 2.7
3.0 (2016-03-23)
@@ -324,15 +349,20 @@ installing WhiteNoise and Brotli together like this:
* The setting ``WHITENOISE_GZIP_EXCLUDE_EXTENSIONS`` has been renamed to
``WHITENOISE_SKIP_COMPRESS_EXTENSIONS``.
+
* The CLI :ref:`compression utility <cli-utility>` has moved from ``python -m whitenoise.gzip``
to ``python -m whitenoise.compress``.
+
* The now redundant ``gzipstatic`` management command has been removed.
+
* WhiteNoise no longer uses the system mimetypes files, so if you are serving
particularly obscure filetypes you may need to add their mimetypes explicitly
using the new :any:`mimetypes <WHITENOISE_MIMETYPES>` setting.
+
* Older versions of Django (1.4-1.7) and Python (2.6) are no longer supported.
If you need support for these platforms you can continue to use `WhiteNoise
2.x`_.
+
* The ``whitenoise.django.GzipManifestStaticFilesStorage`` storage backend
has been moved to
``whitenoise.storage.CompressedManifestStaticFilesStorage``. The old
@@ -386,8 +416,7 @@ needed.
.. rubric:: Thanks
-A big thank-you to `Ed Morley <https://github.com/edmorley>`_ and `Tim Graham
-<https://github.com/timgraham>`_ for their contributions to this release.
+A big thank-you to `Ed Morley <https://github.com/edmorley>`_ and `Tim Graham <https://github.com/timgraham>`_ for their contributions to this release.
2.0.6 (2015-11-15)
------------------
@@ -407,26 +436,32 @@ A big thank-you to `Ed Morley <https://github.com/edmorley>`_ and `Tim Graham
2.0.3 (2015-08-18)
------------------
-* Add `__version__` attribute.
+* Add ``__version__`` attribute.
2.0.2 (2015-07-03)
------------------
-* More helpful error message when STATIC_URL is set to the root of a domain (thanks @dominicrodger).
+* More helpful error message when ``STATIC_URL`` is set to the root of a domain (thanks @dominicrodger).
2.0.1 (2015-06-28)
------------------
* Add support for Python 2.6.
-* Add a more helpful error message when attempting to import DjangoWhiteNoise before `DJANGO_SETTINGS_MODULE` is defined.
+
+* Add a more helpful error message when attempting to import DjangoWhiteNoise before ``DJANGO_SETTINGS_MODULE`` is defined.
2.0 (2015-06-20)
----------------
-* Add an `autorefresh` mode which picks up changes to static files made after application startup (for use in development).
-* Add a `use_finders` mode for DjangoWhiteNoise which finds files in their original directories without needing them collected in `STATIC_ROOT` (for use in development). Note, this is only useful if you don't want to use Django's default runserver behaviour.
-* Remove the `follow_symlinks` argument from `add_files` and now always follow symlinks.
+* Add an ``autorefresh`` mode which picks up changes to static files made after application startup (for use in development).
+
+* Add a ``use_finders`` mode for DjangoWhiteNoise which finds files in their original directories without needing them collected in ``STATIC_ROOT`` (for use in development).
+ Note, this is only useful if you don't want to use Django's default runserver behaviour.
+
+* Remove the ``follow_symlinks`` argument from ``add_files`` and now always follow symlinks.
+
* Support extra mimetypes which Python doesn't know about by default (including .woff2 format)
+
* Some internal refactoring. Note, if you subclass WhiteNoise to add custom behaviour you may need to make some small changes to your code.
1.0.6 (2014-12-12)
@@ -443,7 +478,9 @@ A big thank-you to `Ed Morley <https://github.com/edmorley>`_ and `Tim Graham
------------------
* Don't attempt to gzip ``.woff`` files as they're already compressed.
+
* Base decision to gzip on compression ratio achieved, so we don't incur gzip overhead just to save a few bytes.
+
* More helpful error message from ``collectstatic`` if CSS files reference missing assets.
1.0.3 (2014-06-08)
@@ -460,6 +497,7 @@ A big thank-you to `Ed Morley <https://github.com/edmorley>`_ and `Tim Graham
------------------
* Fix path-to-URL conversion for Windows.
+
* Remove cruft from packaging manifest.
1.0 (2014-04-14)
diff --git a/docs/conf.py b/docs/conf.py
index 80584a9..e3fe066 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,16 +1,3 @@
-# flake8: noqa
-#
-# WhiteNoise documentation build configuration file, created by
-# sphinx-quickstart on Sun Aug 11 15:22:49 2013.
-#
-# This file is execfile()d with the current directory set to its containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
from __future__ import annotations
import datetime
@@ -32,9 +19,6 @@ sys.path.insert(0, os.path.join(project_root, "src"))
# -- General configuration -----------------------------------------------------
-# If your documentation needs a minimal Sphinx version, state it here.
-# needs_sphinx = '1.0'
-
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
@@ -79,16 +63,6 @@ version = _get_version()
# The full version, including alpha/beta/rc tags.
release = version
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-# language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-# today = ''
-# Else, today_fmt is used as the format for a strftime call.
-# today_fmt = '%B %d, %Y'
-
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = [
@@ -96,156 +70,43 @@ exclude_patterns = [
"venv",
]
-# The reST default role (used for this markup: `text`) to use for all documents.
-# default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-# add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-# add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-# show_authors = False
-
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
-# A list of ignored prefixes for module index sorting.
-# modindex_common_prefix = []
-
-
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = "furo"
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further. For a list of options available for each theme, see the
-# documentation.
-# html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-# html_theme_path = []
-
-# The name for this set of Sphinx documents. If None, it defaults to
-# "<project> v<release> documentation".
-# html_title = None
-
-# A shorter title for the navigation bar. Default is the same as html_title.
-# html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-# html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-# html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-# html_static_path = ["_static"]
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-# html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-# html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-# html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-# html_additional_pages = {}
-
-# If false, no module index is generated.
-# html_domain_indices = True
-
-# If false, no index is generated.
-# html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-# html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-# html_show_sourcelink = True
-
-# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-# html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-# html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it. The value of this option must be the
-# base URL from which the finished HTML is served.
-# html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-# html_file_suffix = None
+html_theme_options = {
+ "dark_css_variables": {
+ "admonition-font-size": "100%",
+ "admonition-title-font-size": "100%",
+ },
+ "light_css_variables": {
+ "admonition-font-size": "100%",
+ "admonition-title-font-size": "100%",
+ },
+}
# Output file base name for HTML help builder.
htmlhelp_basename = "WhiteNoisedoc"
-
# -- Options for LaTeX output --------------------------------------------------
-latex_elements = {
- # The paper size ('letterpaper' or 'a4paper').
- #'papersize': 'letterpaper',
- # The font size ('10pt', '11pt' or '12pt').
- #'pointsize': '10pt',
- # Additional stuff for the LaTeX preamble.
- #'preamble': '',
-}
-
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
("index", "WhiteNoise.tex", "WhiteNoise Documentation", "David Evans", "manual")
]
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-# latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-# latex_use_parts = False
-
-# If true, show page references after internal links.
-# latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-# latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-# latex_appendices = []
-
-# If false, no module index is generated.
-# latex_domain_indices = True
-
-
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [("index", "whitenoise", "WhiteNoise Documentation", ["David Evans"], 1)]
-# If true, show URL addresses after external links.
-# man_show_urls = False
-
-
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
@@ -263,15 +124,6 @@ texinfo_documents = [
)
]
-# Documents to append as an appendix to all manuals.
-# texinfo_appendices = []
-
-# If false, no module index is generated.
-# texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-# texinfo_show_urls = 'footnote'
-
-git_tag = f"v{version}" if version != "development" else "master"
-github_base_url = f"https://github.com/evansd/whitenoise/blob/{git_tag}/"
-extlinks = {"file": (github_base_url + "%s", "")}
+git_tag = f"{version}" if version != "development" else "main"
+github_base_url = f"https://github.com/evansd/whitenoise/blob/{git_tag}/src/"
+extlinks = {"ghfile": (github_base_url + "%s", "")}
diff --git a/docs/django.rst b/docs/django.rst
index a1450b0..3b6e642 100644
--- a/docs/django.rst
+++ b/docs/django.rst
@@ -32,10 +32,10 @@ rather than writing the URL directly. For example:
.. code-block:: django
{% load static %}
- <img src="{% static "images/hi.jpg" %}" alt="Hi!" />
+ <img src="{% static "images/hi.jpg" %}" alt="Hi!">
<!-- DON'T WRITE THIS -->
- <img src="/static/images/hi.jpg" alt="Hi!" />
+ <img src="/static/images/hi.jpg" alt="Hi!">
For further details see the Django `staticfiles
<https://docs.djangoproject.com/en/stable/howto/static-files/>`_ guide.
@@ -81,9 +81,22 @@ caching.
3. Add compression and caching support
--------------------------------------
-WhiteNoise comes with a storage backend which automatically takes care of
-compressing your files and creating unique names for each version so they can
-safely be cached forever. To use it, just add this to your ``settings.py``:
+WhiteNoise comes with a storage backend which compresses your files and hashes
+them to unique names, so they can safely be cached forever. To use it, set it
+as your staticfiles storage backend in your settings file.
+
+On Django 4.2+:
+
+.. code-block:: python
+
+ STORAGES = {
+ # ...
+ "staticfiles": {
+ "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
+ },
+ }
+
+On older Django versions:
.. code-block:: python
@@ -91,14 +104,16 @@ safely be cached forever. To use it, just add this to your ``settings.py``:
This combines automatic compression with the caching behaviour provided by
Django's ManifestStaticFilesStorage_ backend. If you want to apply compression
-but don't want the caching behaviour then you can use:
+but don't want the caching behaviour then you can use the alternative backend:
.. code-block:: python
- STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
+ "whitenoise.storage.CompressedStaticFilesStorage"
+
+.. note::
-.. note:: If you are having problems after switching to the WhiteNoise storage
- backend please see the :ref:`troubleshooting guide <storage-troubleshoot>`.
+ If you are having problems after switching to the WhiteNoise storage
+ backend please see the :ref:`troubleshooting guide <storage-troubleshoot>`.
.. _ManifestStaticFilesStorage: https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/#manifeststaticfilesstorage
@@ -295,7 +310,7 @@ arguments upper-cased with a 'WHITENOISE\_' prefix.
Time (in seconds) for which browsers and proxies should cache **non-versioned** files.
- Versioned files (i.e. files which have been given a unique name like *base.a4ef2389.css* by
+ Versioned files (i.e. files which have been given a unique name like ``base.a4ef2389.css`` by
including a hash of their contents in the name) are detected automatically and set to be
cached forever.
@@ -303,6 +318,7 @@ arguments upper-cased with a 'WHITENOISE\_' prefix.
long enough that, if you're running WhiteNoise behind a CDN, the CDN will still take
the majority of the strain during times of heavy load.
+ Set to ``None`` to disable setting any ``Cache-Control`` header on non-versioned files.
.. attribute:: WHITENOISE_INDEX_FILE
@@ -324,7 +340,7 @@ arguments upper-cased with a 'WHITENOISE\_' prefix.
Note that WhiteNoise ships with its own default set of mimetypes and does
not use the system-supplied ones (e.g. ``/etc/mime.types``). This ensures
that it behaves consistently regardless of the environment in which it's
- run. View the defaults in the :file:`media_types.py
+ run. View the defaults in the :ghfile:`media_types.py
<whitenoise/media_types.py>` file.
In addition to file extensions, mimetypes can be specified by supplying the entire
@@ -408,7 +424,7 @@ arguments upper-cased with a 'WHITENOISE\_' prefix.
.. attribute:: WHITENOISE_IMMUTABLE_FILE_TEST
- :default: See :file:`immutable_file_test <whitenoise/middleware.py#L121>` in source
+ :default: See :ghfile:`immutable_file_test <whitenoise/middleware.py#L134>` in source
Reference to function, or string.
@@ -515,7 +531,7 @@ files after startup (unless using Django `DEBUG` mode). As such, all static
files must be generated in advance. If you're using Django Compressor, this
can be performed using its `offline compression`_ feature.
-.. _offline compression: https://django-compressor.readthedocs.io/en/latest/usage/#offline-compression
+.. _offline compression: https://django-compressor.readthedocs.io/en/stable/usage.html#offline-compression
--------------------------------------------------------------------------
@@ -662,7 +678,7 @@ your static directory to just the files you need.
Why do I get "ValueError: Missing staticfiles manifest entry for ..."?
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-If you are seeing this error that you means you are referencing a static file in your
+If you are seeing this error that means you are referencing a static file in your
templates (using something like ``{% static "foo" %}`` which doesn't exist, or
at least isn't where Django expects it to be. If you don't understand why Django can't
find the file you can use
diff --git a/docs/index.rst b/docs/index.rst
index 917cb1d..8ded314 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,27 +1,6 @@
WhiteNoise
==========
-.. image:: https://img.shields.io/travis/evansd/whitenoise.svg
- :target: https://travis-ci.org/evansd/whitenoise
- :alt: Build Status (Linux)
-
-.. image:: https://img.shields.io/appveyor/ci/evansd/whitenoise.svg
- :target: https://ci.appveyor.com/project/evansd/whitenoise
- :alt: Build Status (Windows)
-
-.. image:: https://img.shields.io/pypi/v/whitenoise.svg
- :target: https://pypi.python.org/pypi/whitenoise
- :alt: Latest PyPI version
-
-.. image:: https://img.shields.io/pypi/dm/whitenoise.svg
- :target: https://pypistats.org/packages/whitenoise
- :alt: Monthly PyPI downloads
-
-.. image:: https://img.shields.io/github/stars/evansd/whitenoise.svg?style=social&label=Star
- :target: https://github.com/evansd/whitenoise
- :alt: GitHub project
-
-
**Radically simplified static file serving for Python web apps**
With a couple of lines of config WhiteNoise allows your web app to serve its
@@ -116,9 +95,9 @@ Compatibility
-------------
WhiteNoise works with any WSGI-compatible application and is tested on Python
-**3.7** – **3.10**, on both Linux and Windows.
+**3.7** – **3.11**, on both Linux and Windows.
-Django WhiteNoiseMiddlware is tested with Django versions **2.2** --- **4.0**
+Django WhiteNoiseMiddleware is tested with Django versions **3.2** --- **4.1**
Endorsements
diff --git a/pyproject.toml b/pyproject.toml
index 446f713..9fb6e9f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,14 +1,12 @@
[build-system]
-requires = ["setuptools >= 40.6.0", "wheel"]
build-backend = "setuptools.build_meta"
+requires = [
+ "setuptools",
+]
[tool.black]
target-version = ['py37']
-[tool.isort]
-profile = "black"
-add_imports = "from __future__ import annotations"
-
[tool.pytest.ini_options]
addopts = """\
--strict-config
diff --git a/requirements/compile.py b/requirements/compile.py
index 111b0f8..df7326b 100755
--- a/requirements/compile.py
+++ b/requirements/compile.py
@@ -9,7 +9,7 @@ from pathlib import Path
if __name__ == "__main__":
os.chdir(Path(__file__).parent)
os.environ["CUSTOM_COMPILE_COMMAND"] = "requirements/compile.py"
- os.environ.pop("PIP_REQUIRE_VIRTUALENV", None)
+ os.environ["PIP_REQUIRE_VIRTUALENV"] = "0"
common_args = [
"-m",
"piptools",
@@ -22,45 +22,33 @@ if __name__ == "__main__":
"python3.7",
*common_args,
"-P",
- "Django>=2.2,<2.3",
- "-o",
- "py37-django22.txt",
- ],
- check=True,
- capture_output=True,
- )
- subprocess.run(
- [
- "python3.7",
- *common_args,
- "-P",
- "Django>=3.0a1,<3.1",
+ "Django>=3.2a1,<3.3",
"-o",
- "py37-django30.txt",
+ "py37-django32.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.7",
+ "python3.8",
*common_args,
"-P",
- "Django>=3.1a1,<3.2",
+ "Django>=3.2a1,<3.3",
"-o",
- "py37-django31.txt",
+ "py38-django32.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.7",
+ "python3.8",
*common_args,
"-P",
- "Django>=3.2a1,<3.3",
+ "Django>=4.0a1,<4.1",
"-o",
- "py37-django32.txt",
+ "py38-django40.txt",
],
check=True,
capture_output=True,
@@ -70,9 +58,9 @@ if __name__ == "__main__":
"python3.8",
*common_args,
"-P",
- "Django>=2.2,<2.3",
+ "Django>=4.1a1,<4.2",
"-o",
- "py38-django22.txt",
+ "py38-django41.txt",
],
check=True,
capture_output=True,
@@ -82,45 +70,45 @@ if __name__ == "__main__":
"python3.8",
*common_args,
"-P",
- "Django>=3.0a1,<3.1",
+ "Django>=4.2a1,<5.0",
"-o",
- "py38-django30.txt",
+ "py38-django42.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.8",
+ "python3.9",
*common_args,
"-P",
- "Django>=3.1a1,<3.2",
+ "Django>=3.2a1,<3.3",
"-o",
- "py38-django31.txt",
+ "py39-django32.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.8",
+ "python3.9",
*common_args,
"-P",
- "Django>=3.2a1,<3.3",
+ "Django>=4.0a1,<4.1",
"-o",
- "py38-django32.txt",
+ "py39-django40.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.8",
+ "python3.9",
*common_args,
"-P",
- "Django>=4.0a1,<4.1",
+ "Django>=4.1a1,<4.2",
"-o",
- "py38-django40.txt",
+ "py39-django41.txt",
],
check=True,
capture_output=True,
@@ -130,81 +118,81 @@ if __name__ == "__main__":
"python3.9",
*common_args,
"-P",
- "Django>=2.2,<2.3",
+ "Django>=4.2a1,<5.0",
"-o",
- "py39-django22.txt",
+ "py39-django42.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.9",
+ "python3.10",
*common_args,
"-P",
- "Django>=3.0a1,<3.1",
+ "Django>=3.2a1,<3.3",
"-o",
- "py39-django30.txt",
+ "py310-django32.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.9",
+ "python3.10",
*common_args,
"-P",
- "Django>=3.1a1,<3.2",
+ "Django>=4.0a1,<4.1",
"-o",
- "py39-django31.txt",
+ "py310-django40.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.9",
+ "python3.10",
*common_args,
"-P",
- "Django>=3.2a1,<3.3",
+ "Django>=4.1a1,<4.2",
"-o",
- "py39-django32.txt",
+ "py310-django41.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.9",
+ "python3.10",
*common_args,
"-P",
- "Django>=4.0a1,<4.1",
+ "Django>=4.2a1,<5.0",
"-o",
- "py39-django40.txt",
+ "py310-django42.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.10",
+ "python3.11",
*common_args,
"-P",
- "Django>=3.2a1,<3.3",
+ "Django>=4.1a1,<4.2",
"-o",
- "py310-django32.txt",
+ "py311-django41.txt",
],
check=True,
capture_output=True,
)
subprocess.run(
[
- "python3.10",
+ "python3.11",
*common_args,
"-P",
- "Django>=4.0a1,<4.1",
+ "Django>=4.2a1,<5.0",
"-o",
- "py310-django40.txt",
+ "py311-django42.txt",
],
check=True,
capture_output=True,
diff --git a/requirements/py310-django32.txt b/requirements/py310-django32.txt
index 60cf11e..c629351 100644
--- a/requirements/py310-django32.txt
+++ b/requirements/py310-django32.txt
@@ -1,48 +1,46 @@
#
-# This file is autogenerated by pip-compile with python 3.10
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==3.2.12
+django==3.2.18
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-pytz==2021.3
+pytz==2022.7.1
# via django
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
diff --git a/requirements/py310-django40.txt b/requirements/py310-django40.txt
index ad0c4c8..124b1d4 100644
--- a/requirements/py310-django40.txt
+++ b/requirements/py310-django40.txt
@@ -1,46 +1,44 @@
#
-# This file is autogenerated by pip-compile with python 3.10
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==4.0.2
+django==4.0.10
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
diff --git a/requirements/py310-django41.txt b/requirements/py310-django41.txt
new file mode 100644
index 0000000..38349b5
--- /dev/null
+++ b/requirements/py310-django41.txt
@@ -0,0 +1,44 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+# requirements/compile.py
+#
+asgiref==3.6.0
+ # via django
+attrs==22.2.0
+ # via pytest
+brotli==1.0.9
+ # via -r requirements.in
+certifi==2022.12.7
+ # via requests
+charset-normalizer==3.0.1
+ # via requests
+coverage==7.1.0
+ # via -r requirements.in
+django==4.1.7
+ # via -r requirements.in
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
+ # via requests
+iniconfig==2.0.0
+ # via pytest
+packaging==23.0
+ # via pytest
+pluggy==1.0.0
+ # via pytest
+pytest==7.2.1
+ # via
+ # -r requirements.in
+ # pytest-randomly
+pytest-randomly==3.12.0
+ # via -r requirements.in
+requests==2.28.2
+ # via -r requirements.in
+sqlparse==0.4.3
+ # via django
+tomli==2.0.1
+ # via pytest
+urllib3==1.26.14
+ # via requests
diff --git a/requirements/py310-django42.txt b/requirements/py310-django42.txt
new file mode 100644
index 0000000..a1c89a4
--- /dev/null
+++ b/requirements/py310-django42.txt
@@ -0,0 +1,44 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+# requirements/compile.py
+#
+asgiref==3.6.0
+ # via django
+attrs==22.2.0
+ # via pytest
+brotli==1.0.9
+ # via -r requirements.in
+certifi==2022.12.7
+ # via requests
+charset-normalizer==3.0.1
+ # via requests
+coverage==7.1.0
+ # via -r requirements.in
+django==4.2b1
+ # via -r requirements.in
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
+ # via requests
+iniconfig==2.0.0
+ # via pytest
+packaging==23.0
+ # via pytest
+pluggy==1.0.0
+ # via pytest
+pytest==7.2.1
+ # via
+ # -r requirements.in
+ # pytest-randomly
+pytest-randomly==3.12.0
+ # via -r requirements.in
+requests==2.28.2
+ # via -r requirements.in
+sqlparse==0.4.3
+ # via django
+tomli==2.0.1
+ # via pytest
+urllib3==1.26.14
+ # via requests
diff --git a/requirements/py311-django41.txt b/requirements/py311-django41.txt
new file mode 100644
index 0000000..89ddfe0
--- /dev/null
+++ b/requirements/py311-django41.txt
@@ -0,0 +1,40 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# requirements/compile.py
+#
+asgiref==3.6.0
+ # via django
+attrs==22.2.0
+ # via pytest
+brotli==1.0.9
+ # via -r requirements.in
+certifi==2022.12.7
+ # via requests
+charset-normalizer==3.0.1
+ # via requests
+coverage==7.1.0
+ # via -r requirements.in
+django==4.1.7
+ # via -r requirements.in
+idna==3.4
+ # via requests
+iniconfig==2.0.0
+ # via pytest
+packaging==23.0
+ # via pytest
+pluggy==1.0.0
+ # via pytest
+pytest==7.2.1
+ # via
+ # -r requirements.in
+ # pytest-randomly
+pytest-randomly==3.12.0
+ # via -r requirements.in
+requests==2.28.2
+ # via -r requirements.in
+sqlparse==0.4.3
+ # via django
+urllib3==1.26.14
+ # via requests
diff --git a/requirements/py311-django42.txt b/requirements/py311-django42.txt
new file mode 100644
index 0000000..773991c
--- /dev/null
+++ b/requirements/py311-django42.txt
@@ -0,0 +1,40 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# requirements/compile.py
+#
+asgiref==3.6.0
+ # via django
+attrs==22.2.0
+ # via pytest
+brotli==1.0.9
+ # via -r requirements.in
+certifi==2022.12.7
+ # via requests
+charset-normalizer==3.0.1
+ # via requests
+coverage==7.1.0
+ # via -r requirements.in
+django==4.2b1
+ # via -r requirements.in
+idna==3.4
+ # via requests
+iniconfig==2.0.0
+ # via pytest
+packaging==23.0
+ # via pytest
+pluggy==1.0.0
+ # via pytest
+pytest==7.2.1
+ # via
+ # -r requirements.in
+ # pytest-randomly
+pytest-randomly==3.12.0
+ # via -r requirements.in
+requests==2.28.2
+ # via -r requirements.in
+sqlparse==0.4.3
+ # via django
+urllib3==1.26.14
+ # via requests
diff --git a/requirements/py37-django22.txt b/requirements/py37-django22.txt
deleted file mode 100644
index cc6183d..0000000
--- a/requirements/py37-django22.txt
+++ /dev/null
@@ -1,55 +0,0 @@
-#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
-#
-# requirements/compile.py
-#
-attrs==21.4.0
- # via pytest
-brotli==1.0.9
- # via -r requirements.in
-certifi==2021.10.8
- # via requests
-charset-normalizer==2.0.11
- # via requests
-coverage==6.3
- # via -r requirements.in
-django==2.2.27
- # via -r requirements.in
-idna==3.3
- # via requests
-importlib-metadata==4.10.1
- # via
- # pluggy
- # pytest
- # pytest-randomly
-iniconfig==1.1.1
- # via pytest
-packaging==21.3
- # via pytest
-pluggy==1.0.0
- # via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
- # via
- # -r requirements.in
- # pytest-randomly
-pytest-randomly==3.11.0
- # via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
- # via -r requirements.in
-sqlparse==0.4.2
- # via django
-toml==0.10.2
- # via pytest
-typing-extensions==4.0.1
- # via importlib-metadata
-urllib3==1.26.8
- # via requests
-zipp==3.7.0
- # via importlib-metadata
diff --git a/requirements/py37-django30.txt b/requirements/py37-django30.txt
deleted file mode 100644
index a5e5c5c..0000000
--- a/requirements/py37-django30.txt
+++ /dev/null
@@ -1,59 +0,0 @@
-#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
-#
-# requirements/compile.py
-#
-asgiref==3.5.0
- # via django
-attrs==21.4.0
- # via pytest
-brotli==1.0.9
- # via -r requirements.in
-certifi==2021.10.8
- # via requests
-charset-normalizer==2.0.11
- # via requests
-coverage==6.3
- # via -r requirements.in
-django==3.0.14
- # via -r requirements.in
-idna==3.3
- # via requests
-importlib-metadata==4.10.1
- # via
- # pluggy
- # pytest
- # pytest-randomly
-iniconfig==1.1.1
- # via pytest
-packaging==21.3
- # via pytest
-pluggy==1.0.0
- # via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
- # via
- # -r requirements.in
- # pytest-randomly
-pytest-randomly==3.11.0
- # via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
- # via -r requirements.in
-sqlparse==0.4.2
- # via django
-toml==0.10.2
- # via pytest
-typing-extensions==4.0.1
- # via
- # asgiref
- # importlib-metadata
-urllib3==1.26.8
- # via requests
-zipp==3.7.0
- # via importlib-metadata
diff --git a/requirements/py37-django31.txt b/requirements/py37-django31.txt
deleted file mode 100644
index d1a26b1..0000000
--- a/requirements/py37-django31.txt
+++ /dev/null
@@ -1,59 +0,0 @@
-#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
-#
-# requirements/compile.py
-#
-asgiref==3.5.0
- # via django
-attrs==21.4.0
- # via pytest
-brotli==1.0.9
- # via -r requirements.in
-certifi==2021.10.8
- # via requests
-charset-normalizer==2.0.11
- # via requests
-coverage==6.3
- # via -r requirements.in
-django==3.1.14
- # via -r requirements.in
-idna==3.3
- # via requests
-importlib-metadata==4.10.1
- # via
- # pluggy
- # pytest
- # pytest-randomly
-iniconfig==1.1.1
- # via pytest
-packaging==21.3
- # via pytest
-pluggy==1.0.0
- # via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
- # via
- # -r requirements.in
- # pytest-randomly
-pytest-randomly==3.11.0
- # via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
- # via -r requirements.in
-sqlparse==0.4.2
- # via django
-toml==0.10.2
- # via pytest
-typing-extensions==4.0.1
- # via
- # asgiref
- # importlib-metadata
-urllib3==1.26.8
- # via requests
-zipp==3.7.0
- # via importlib-metadata
diff --git a/requirements/py37-django32.txt b/requirements/py37-django32.txt
index b698d91..621b875 100644
--- a/requirements/py37-django32.txt
+++ b/requirements/py37-django32.txt
@@ -1,59 +1,57 @@
#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.7
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==3.2.12
+django==3.2.18
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via
# pluggy
# pytest
# pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-pytz==2021.3
+pytz==2022.7.1
# via django
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-typing-extensions==4.0.1
+typing-extensions==4.5.0
# via
# asgiref
# importlib-metadata
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/requirements/py38-django31.txt b/requirements/py38-django31.txt
deleted file mode 100644
index 6e37c55..0000000
--- a/requirements/py38-django31.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-#
-# This file is autogenerated by pip-compile with python 3.8
-# To update, run:
-#
-# requirements/compile.py
-#
-asgiref==3.5.0
- # via django
-attrs==21.4.0
- # via pytest
-brotli==1.0.9
- # via -r requirements.in
-certifi==2021.10.8
- # via requests
-charset-normalizer==2.0.11
- # via requests
-coverage==6.3
- # via -r requirements.in
-django==3.1.14
- # via -r requirements.in
-idna==3.3
- # via requests
-importlib-metadata==4.10.1
- # via pytest-randomly
-iniconfig==1.1.1
- # via pytest
-packaging==21.3
- # via pytest
-pluggy==1.0.0
- # via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
- # via
- # -r requirements.in
- # pytest-randomly
-pytest-randomly==3.11.0
- # via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
- # via -r requirements.in
-sqlparse==0.4.2
- # via django
-toml==0.10.2
- # via pytest
-urllib3==1.26.8
- # via requests
-zipp==3.7.0
- # via importlib-metadata
diff --git a/requirements/py38-django32.txt b/requirements/py38-django32.txt
index 1fd72be..1097620 100644
--- a/requirements/py38-django32.txt
+++ b/requirements/py38-django32.txt
@@ -1,52 +1,50 @@
#
-# This file is autogenerated by pip-compile with python 3.8
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==3.2.12
+django==3.2.18
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-pytz==2021.3
+pytz==2022.7.1
# via django
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/requirements/py38-django40.txt b/requirements/py38-django40.txt
index ff76320..060c87d 100644
--- a/requirements/py38-django40.txt
+++ b/requirements/py38-django40.txt
@@ -1,52 +1,50 @@
#
-# This file is autogenerated by pip-compile with python 3.8
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
-backports.zoneinfo==0.2.1
+backports-zoneinfo==0.2.1
# via django
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==4.0.2
+django==4.0.10
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/requirements/py39-django30.txt b/requirements/py38-django41.txt
similarity index 56%
rename from requirements/py39-django30.txt
rename to requirements/py38-django41.txt
index 88838c6..5ecc293 100644
--- a/requirements/py39-django30.txt
+++ b/requirements/py38-django41.txt
@@ -1,52 +1,50 @@
#
-# This file is autogenerated by pip-compile with python 3.9
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
+backports-zoneinfo==0.2.1
+ # via django
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==3.0.14
+django==4.1.7
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/requirements/py38-django30.txt b/requirements/py38-django42.txt
similarity index 56%
rename from requirements/py38-django30.txt
rename to requirements/py38-django42.txt
index fbe06e3..ea4b26b 100644
--- a/requirements/py38-django30.txt
+++ b/requirements/py38-django42.txt
@@ -1,52 +1,50 @@
#
-# This file is autogenerated by pip-compile with python 3.8
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
+backports-zoneinfo==0.2.1
+ # via django
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==3.0.14
+django==4.2b1
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/requirements/py39-django31.txt b/requirements/py39-django31.txt
deleted file mode 100644
index f1fc3c4..0000000
--- a/requirements/py39-django31.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-#
-# This file is autogenerated by pip-compile with python 3.9
-# To update, run:
-#
-# requirements/compile.py
-#
-asgiref==3.5.0
- # via django
-attrs==21.4.0
- # via pytest
-brotli==1.0.9
- # via -r requirements.in
-certifi==2021.10.8
- # via requests
-charset-normalizer==2.0.11
- # via requests
-coverage==6.3
- # via -r requirements.in
-django==3.1.14
- # via -r requirements.in
-idna==3.3
- # via requests
-importlib-metadata==4.10.1
- # via pytest-randomly
-iniconfig==1.1.1
- # via pytest
-packaging==21.3
- # via pytest
-pluggy==1.0.0
- # via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
- # via
- # -r requirements.in
- # pytest-randomly
-pytest-randomly==3.11.0
- # via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
- # via -r requirements.in
-sqlparse==0.4.2
- # via django
-toml==0.10.2
- # via pytest
-urllib3==1.26.8
- # via requests
-zipp==3.7.0
- # via importlib-metadata
diff --git a/requirements/py39-django32.txt b/requirements/py39-django32.txt
index 7924a3e..31406fc 100644
--- a/requirements/py39-django32.txt
+++ b/requirements/py39-django32.txt
@@ -1,52 +1,50 @@
#
-# This file is autogenerated by pip-compile with python 3.9
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==3.2.12
+django==3.2.18
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-pytz==2021.3
+pytz==2022.7.1
# via django
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/requirements/py39-django40.txt b/requirements/py39-django40.txt
index 43f276d..147abd9 100644
--- a/requirements/py39-django40.txt
+++ b/requirements/py39-django40.txt
@@ -1,50 +1,48 @@
#
-# This file is autogenerated by pip-compile with python 3.9
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
#
# requirements/compile.py
#
-asgiref==3.5.0
+asgiref==3.6.0
# via django
-attrs==21.4.0
+attrs==22.2.0
# via pytest
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==4.0.2
+django==4.0.10
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/requirements/py39-django22.txt b/requirements/py39-django41.txt
similarity index 56%
rename from requirements/py39-django22.txt
rename to requirements/py39-django41.txt
index 1a6c57a..de60b3d 100644
--- a/requirements/py39-django22.txt
+++ b/requirements/py39-django41.txt
@@ -1,50 +1,48 @@
#
-# This file is autogenerated by pip-compile with python 3.9
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
#
# requirements/compile.py
#
-attrs==21.4.0
+asgiref==3.6.0
+ # via django
+attrs==22.2.0
# via pytest
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==2.2.27
+django==4.1.7
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/requirements/py38-django22.txt b/requirements/py39-django42.txt
similarity index 56%
rename from requirements/py38-django22.txt
rename to requirements/py39-django42.txt
index f2301b0..eff809f 100644
--- a/requirements/py38-django22.txt
+++ b/requirements/py39-django42.txt
@@ -1,50 +1,48 @@
#
-# This file is autogenerated by pip-compile with python 3.8
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
#
# requirements/compile.py
#
-attrs==21.4.0
+asgiref==3.6.0
+ # via django
+attrs==22.2.0
# via pytest
brotli==1.0.9
# via -r requirements.in
-certifi==2021.10.8
+certifi==2022.12.7
# via requests
-charset-normalizer==2.0.11
+charset-normalizer==3.0.1
# via requests
-coverage==6.3
+coverage==7.1.0
# via -r requirements.in
-django==2.2.27
+django==4.2b1
# via -r requirements.in
-idna==3.3
+exceptiongroup==1.1.0
+ # via pytest
+idna==3.4
# via requests
-importlib-metadata==4.10.1
+importlib-metadata==6.0.0
# via pytest-randomly
-iniconfig==1.1.1
+iniconfig==2.0.0
# via pytest
-packaging==21.3
+packaging==23.0
# via pytest
pluggy==1.0.0
# via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.7
- # via packaging
-pytest==6.2.5
+pytest==7.2.1
# via
# -r requirements.in
# pytest-randomly
-pytest-randomly==3.11.0
+pytest-randomly==3.12.0
# via -r requirements.in
-pytz==2021.3
- # via django
-requests==2.27.1
+requests==2.28.2
# via -r requirements.in
-sqlparse==0.4.2
+sqlparse==0.4.3
# via django
-toml==0.10.2
+tomli==2.0.1
# via pytest
-urllib3==1.26.8
+urllib3==1.26.14
# via requests
-zipp==3.7.0
+zipp==3.14.0
# via importlib-metadata
diff --git a/scripts/generate_default_media_types.py b/scripts/generate_default_media_types.py
index f8e0b86..0d69174 100755
--- a/scripts/generate_default_media_types.py
+++ b/scripts/generate_default_media_types.py
@@ -11,7 +11,7 @@ module_dir = Path(__file__).parent.resolve()
media_types_py = module_dir / "../src/whitenoise/media_types.py"
-def main():
+def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
@@ -50,7 +50,7 @@ EXTRA_MIMETYPES = {
FUNCTION_TEMPLATE = '''\
-def default_types():
+def default_types() -> dict[str, str]:
"""
We use our own set of default media types rather than the system-supplied
ones. This ensures consistent media type behaviour across varied
@@ -64,16 +64,16 @@ def default_types():
}}'''
-def get_default_types_function():
+def get_default_types_function() -> str:
types_map = get_types_map()
lines = [
- f' "{suffix}": "{media_type}",'
+ f' "{suffix}": "{media_type}",' # noqa: B028
for suffix, media_type in types_map.items()
]
return FUNCTION_TEMPLATE.format(entries="\n".join(lines))
-def get_types_map():
+def get_types_map() -> dict[str, str]:
nginx_data = get_nginx_data()
matches = re.findall(r"(\w+/.*?)\s+(.*?);", nginx_data)
types_map = {}
@@ -91,7 +91,7 @@ def get_types_map():
return dict(sorted(types_map.items()))
-def get_nginx_data():
+def get_nginx_data() -> str:
conn = http.client.HTTPSConnection("raw.githubusercontent.com")
with closing(conn):
conn.request("GET", "/nginx/nginx/master/conf/mime.types")
diff --git a/setup.cfg b/setup.cfg
index 6ff8fbd..fe288bb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,52 +1,54 @@
[metadata]
name = whitenoise
-version = 6.0.0
+version = 6.4.0
description = Radically simplified static file serving for WSGI applications
long_description = file: README.rst
long_description_content_type = text/x-rst
+url = https://github.com/evansd/whitenoise
author = David Evans
author_email = d@evans.io
-url = https://whitenoise.evans.io
-project_urls =
- Documentation = https://whitenoise.evans.io/
- Changelog = https://whitenoise.evans.io/en/stable/changelog.html
license = MIT
-keywords = Django
+license_file = LICENSE
classifiers =
Development Status :: 5 - Production/Stable
Framework :: Django
- Framework :: Django :: 2.2
- Framework :: Django :: 3.0
- Framework :: Django :: 3.1
Framework :: Django :: 3.2
Framework :: Django :: 4.0
+ Framework :: Django :: 4.1
+ Framework :: Django :: 4.2
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: OS Independent
- Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3
+ Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
+ Programming Language :: Python :: Implementation :: CPython
Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
-license_file = LICENSE
+ Typing :: Typed
+keywords = Django
+project_urls =
+ Documentation = https://whitenoise.evans.io/
+ Changelog = https://whitenoise.evans.io/en/stable/changelog.html
[options]
-package_dir=
- =src
packages = find:
-include_package_data = True
python_requires = >=3.7
+include_package_data = True
+package_dir =
+ =src
zip_safe = False
+[options.packages.find]
+where = src
+
[options.extras_require]
brotli =
Brotli
-[options.packages.find]
-where = src
-
[flake8]
max-line-length = 88
extend-ignore = E203
@@ -62,8 +64,8 @@ source =
[coverage:paths]
source =
- src
- .tox/*/site-packages
+ src
+ .tox/**/site-packages
[coverage:report]
show_missing = True
diff --git a/setup.py b/setup.py
deleted file mode 100644
index a03590f..0000000
--- a/setup.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from __future__ import annotations
-
-from setuptools import setup
-
-setup()
diff --git a/src/whitenoise/base.py b/src/whitenoise/base.py
index e7bcf2a..45c58f9 100644
--- a/src/whitenoise/base.py
+++ b/src/whitenoise/base.py
@@ -4,74 +4,70 @@ import os
import re
import warnings
from posixpath import normpath
+from typing import Callable
from wsgiref.headers import Headers
from wsgiref.util import FileWrapper
from .media_types import MediaTypes
-from .responders import IsDirectoryError, MissingFileError, Redirect, StaticFile
-from .string_utils import (
- decode_if_byte_string,
- decode_path_info,
- ensure_leading_trailing_slash,
-)
+from .responders import IsDirectoryError
+from .responders import MissingFileError
+from .responders import Redirect
+from .responders import StaticFile
+from .string_utils import decode_path_info
+from .string_utils import ensure_leading_trailing_slash
class WhiteNoise:
-
# Ten years is what nginx sets a max age if you use 'expires max;'
# so we'll follow its lead
FOREVER = 10 * 365 * 24 * 60 * 60
- # Attributes that can be set by keyword args in the constructor
- config_attrs = (
- "autorefresh",
- "max_age",
- "allow_all_origins",
- "charset",
- "mimetypes",
- "add_headers_function",
- "index_file",
- "immutable_file_test",
- )
- # Re-check the filesystem on every request so that any changes are
- # automatically picked up. NOTE: For use in development only, not supported
- # in production
- autorefresh = False
- max_age = 60
- # Set 'Access-Control-Allow-Origin: *' header on all files.
- # As these are all public static files this is safe (See
- # https://www.w3.org/TR/cors/#security) and ensures that things (e.g
- # webfonts in Firefox) still work as expected when your static files are
- # served from a CDN, rather than your primary domain.
- allow_all_origins = True
- charset = "utf-8"
- # Custom mime types
- mimetypes = None
- # Callback for adding custom logic when setting headers
- add_headers_function = None
- # Name of index file (None to disable index support)
- index_file = None
+ def __init__(
+ self,
+ application,
+ root=None,
+ prefix=None,
+ *,
+ # Re-check the filesystem on every request so that any changes are
+ # automatically picked up. NOTE: For use in development only, not supported
+ # in production
+ autorefresh: bool = False,
+ max_age: int | None = 60, # seconds
+ # Set 'Access-Control-Allow-Origin: *' header on all files.
+ # As these are all public static files this is safe (See
+ # https://www.w3.org/TR/cors/#security) and ensures that things (e.g
+ # webfonts in Firefox) still work as expected when your static files are
+ # served from a CDN, rather than your primary domain.
+ allow_all_origins: bool = True,
+ charset: str = "utf-8",
+ mimetypes: dict[str, str] | None = None,
+ add_headers_function: Callable[[Headers, str, str], None] | None = None,
+ index_file: str | bool | None = None,
+ immutable_file_test: Callable | str | None = None,
+ ):
+ self.autorefresh = autorefresh
+ self.max_age = max_age
+ self.allow_all_origins = allow_all_origins
+ self.charset = charset
+ self.add_headers_function = add_headers_function
+ if index_file is True:
+ self.index_file: str | None = "index.html"
+ elif isinstance(index_file, str):
+ self.index_file = index_file
+ else:
+ self.index_file = None
- def __init__(self, application, root=None, prefix=None, **kwargs):
- for attr in self.config_attrs:
- try:
- value = kwargs.pop(attr)
- except KeyError:
- pass
+ if immutable_file_test is not None:
+ if not callable(immutable_file_test):
+ regex = re.compile(immutable_file_test)
+ self.immutable_file_test = lambda path, url: bool(regex.search(url))
else:
- value = decode_if_byte_string(value)
- setattr(self, attr, value)
- if kwargs:
- raise TypeError(f"Unexpected keyword argument '{list(kwargs.keys())[0]}'")
- self.media_types = MediaTypes(extra_types=self.mimetypes)
+ self.immutable_file_test = immutable_file_test
+
+ self.media_types = MediaTypes(extra_types=mimetypes)
self.application = application
self.files = {}
self.directories = []
- if self.index_file is True:
- self.index_file = "index.html"
- if not callable(self.immutable_file_test):
- regex = re.compile(self.immutable_file_test)
- self.immutable_file_test = lambda path, url: bool(regex.search(url))
if root is not None:
self.add_files(root, prefix)
@@ -98,10 +94,8 @@ class WhiteNoise:
return []
def add_files(self, root, prefix=None):
- root = decode_if_byte_string(root, force_text=True)
root = os.path.abspath(root)
root = root.rstrip(os.path.sep) + os.path.sep
- prefix = decode_if_byte_string(prefix)
prefix = ensure_leading_trailing_slash(prefix)
if self.autorefresh:
# Later calls to `add_files` overwrite earlier ones, hence we need
@@ -127,7 +121,7 @@ class WhiteNoise:
def add_file_to_dictionary(self, url, path, stat_cache=None):
if self.is_compressed_variant(path, stat_cache=stat_cache):
return
- if self.index_file and url.endswith("/" + self.index_file):
+ if self.index_file is not None and url.endswith("/" + self.index_file):
index_url = url[: -len(self.index_file)]
index_no_slash = index_url.rstrip("/")
self.files[url] = self.redirect(url, index_url)
@@ -138,7 +132,7 @@ class WhiteNoise:
def find_file(self, url):
# Optimization: bail early if the URL can never match a file
- if not self.index_file and url.endswith("/"):
+ if self.index_file is None and url.endswith("/"):
return
if not self.url_is_canonical(url):
return
@@ -158,25 +152,23 @@ class WhiteNoise:
def find_file_at_path(self, path, url):
if self.is_compressed_variant(path):
raise MissingFileError(path)
- if self.index_file:
- return self.find_file_at_path_with_indexes(path, url)
- else:
- return self.get_static_file(path, url)
- def find_file_at_path_with_indexes(self, path, url):
- if url.endswith("/"):
- path = os.path.join(path, self.index_file)
- return self.get_static_file(path, url)
- elif url.endswith("/" + self.index_file):
- if os.path.isfile(path):
- return self.redirect(url, url[: -len(self.index_file)])
- else:
- try:
+ if self.index_file is not None:
+ if url.endswith("/"):
+ path = os.path.join(path, self.index_file)
return self.get_static_file(path, url)
- except IsDirectoryError:
- if os.path.isfile(os.path.join(path, self.index_file)):
- return self.redirect(url, url + "/")
- raise MissingFileError(path)
+ elif url.endswith("/" + self.index_file):
+ if os.path.isfile(path):
+ return self.redirect(url, url[: -len(self.index_file)])
+ else:
+ try:
+ return self.get_static_file(path, url)
+ except IsDirectoryError:
+ if os.path.isfile(os.path.join(path, self.index_file)):
+ return self.redirect(url, url + "/")
+ raise MissingFileError(path)
+
+ return self.get_static_file(path, url)
@staticmethod
def url_is_canonical(url):
@@ -210,7 +202,7 @@ class WhiteNoise:
self.add_cache_headers(headers, path, url)
if self.allow_all_origins:
headers["Access-Control-Allow-Origin"] = "*"
- if self.add_headers_function:
+ if self.add_headers_function is not None:
self.add_headers_function(headers, path, url)
return StaticFile(
path,
diff --git a/src/whitenoise/compress.py b/src/whitenoise/compress.py
index e4e9fba..143e1e4 100644
--- a/src/whitenoise/compress.py
+++ b/src/whitenoise/compress.py
@@ -15,7 +15,6 @@ except ImportError: # pragma: no cover
class Compressor:
-
# Extensions that it's not worth trying to compress
SKIP_COMPRESS_EXTENSIONS = (
# Images
@@ -38,6 +37,18 @@ class Compressor:
# Fonts
"woff",
"woff2",
+ # Video
+ "3gp",
+ "3gpp",
+ "asf",
+ "avi",
+ "m4v",
+ "mov",
+ "mp4",
+ "mpeg",
+ "mpg",
+ "webm",
+ "wmv",
)
def __init__(
diff --git a/src/whitenoise/media_types.py b/src/whitenoise/media_types.py
index cb27af1..d693f3a 100644
--- a/src/whitenoise/media_types.py
+++ b/src/whitenoise/media_types.py
@@ -6,12 +6,12 @@ import os
class MediaTypes:
__slots__ = ("types_map",)
- def __init__(self, *, extra_types=None):
+ def __init__(self, *, extra_types: dict[str, str] | None = None) -> None:
self.types_map = default_types()
if extra_types is not None:
self.types_map.update(extra_types)
- def get_type(self, path):
+ def get_type(self, path: str) -> str:
name = os.path.basename(path).lower()
media_type = self.types_map.get(name)
if media_type is not None:
@@ -20,7 +20,7 @@ class MediaTypes:
return self.types_map.get(extension, "application/octet-stream")
-def default_types():
+def default_types() -> dict[str, str]:
"""
We use our own set of default media types rather than the system-supplied
ones. This ensures consistent media type behaviour across varied
diff --git a/src/whitenoise/middleware.py b/src/whitenoise/middleware.py
index 248339d..3f5a809 100644
--- a/src/whitenoise/middleware.py
+++ b/src/whitenoise/middleware.py
@@ -11,7 +11,7 @@ from django.http import FileResponse
from django.urls import get_script_prefix
from .base import WhiteNoise
-from .string_utils import decode_if_byte_string, ensure_leading_trailing_slash
+from .string_utils import ensure_leading_trailing_slash
__all__ = ["WhiteNoiseMiddleware"]
@@ -31,42 +31,97 @@ class WhiteNoiseFileResponse(FileResponse):
class WhiteNoiseMiddleware(WhiteNoise):
"""
Wrap WhiteNoise to allow it to function as Django middleware, rather
- than WSGI middleware
-
- This functions as both old- and new-style middleware, so can be included in
- either MIDDLEWARE or MIDDLEWARE_CLASSES.
+ than WSGI middleware.
"""
- config_attrs = WhiteNoise.config_attrs + ("root", "use_finders", "static_prefix")
- root = None
- use_finders = False
- static_prefix = None
-
def __init__(self, get_response=None, settings=settings):
self.get_response = get_response
- self.configure_from_settings(settings)
- # Pass None for `application`
- super().__init__(None)
+
+ try:
+ autorefresh: bool = settings.WHITENOISE_AUTOREFRESH
+ except AttributeError:
+ autorefresh = settings.DEBUG
+ try:
+ max_age = settings.WHITENOISE_MAX_AGE
+ except AttributeError:
+ if settings.DEBUG:
+ max_age = 0
+ else:
+ max_age = 60
+ try:
+ allow_all_origins = settings.WHITENOISE_ALLOW_ALL_ORIGINS
+ except AttributeError:
+ allow_all_origins = True
+ try:
+ charset = settings.WHITENOISE_CHARSET
+ except AttributeError:
+ charset = "utf-8"
+ try:
+ mimetypes = settings.WHITENOISE_MIMETYPES
+ except AttributeError:
+ mimetypes = None
+ try:
+ add_headers_function = settings.WHITENOISE_ADD_HEADERS_FUNCTION
+ except AttributeError:
+ add_headers_function = None
+ try:
+ index_file = settings.WHITENOISE_INDEX_FILE
+ except AttributeError:
+ index_file = None
+ try:
+ immutable_file_test = settings.WHITENOISE_IMMUTABLE_FILE_TEST
+ except AttributeError:
+ immutable_file_test = None
+
+ super().__init__(
+ application=None,
+ autorefresh=autorefresh,
+ max_age=max_age,
+ allow_all_origins=allow_all_origins,
+ charset=charset,
+ mimetypes=mimetypes,
+ add_headers_function=add_headers_function,
+ index_file=index_file,
+ immutable_file_test=immutable_file_test,
+ )
+
+ try:
+ self.use_finders = settings.WHITENOISE_USE_FINDERS
+ except AttributeError:
+ self.use_finders = settings.DEBUG
+
+ try:
+ self.static_prefix = settings.WHITENOISE_STATIC_PREFIX
+ except AttributeError:
+ self.static_prefix = urlparse(settings.STATIC_URL or "").path
+ script_prefix = get_script_prefix().rstrip("/")
+ if script_prefix:
+ if self.static_prefix.startswith(script_prefix):
+ self.static_prefix = self.static_prefix[len(script_prefix) :]
+ self.static_prefix = ensure_leading_trailing_slash(self.static_prefix)
+
+ self.static_root = settings.STATIC_ROOT
if self.static_root:
self.add_files(self.static_root, prefix=self.static_prefix)
- if self.root:
- self.add_files(self.root)
+
+ try:
+ root = settings.WHITENOISE_ROOT
+ except AttributeError:
+ root = None
+ if root:
+ self.add_files(root)
+
if self.use_finders and not self.autorefresh:
self.add_files_from_finders()
def __call__(self, request):
- response = self.process_request(request)
- if response is None:
- response = self.get_response(request)
- return response
-
- def process_request(self, request):
if self.autorefresh:
static_file = self.find_file(request.path_info)
else:
static_file = self.files.get(request.path_info)
if static_file is not None:
return self.serve(static_file, request)
+ return self.get_response(request)
@staticmethod
def serve(static_file, request):
@@ -79,30 +134,6 @@ class WhiteNoiseMiddleware(WhiteNoise):
http_response[key] = value
return http_response
- def configure_from_settings(self, settings):
- # Default configuration
- self.autorefresh = settings.DEBUG
- self.use_finders = settings.DEBUG
- self.static_prefix = urlparse(settings.STATIC_URL or "").path
- script_prefix = get_script_prefix().rstrip("/")
- if script_prefix:
- if self.static_prefix.startswith(script_prefix):
- self.static_prefix = self.static_prefix[len(script_prefix) :]
- if settings.DEBUG:
- self.max_age = 0
- # Allow settings to override default attributes
- for attr in self.config_attrs:
- settings_key = f"WHITENOISE_{attr.upper()}"
- try:
- value = getattr(settings, settings_key)
- except AttributeError:
- pass
- else:
- value = decode_if_byte_string(value)
- setattr(self, attr, value)
- self.static_prefix = ensure_leading_trailing_slash(self.static_prefix)
- self.static_root = decode_if_byte_string(settings.STATIC_ROOT)
-
def add_files_from_finders(self):
files = {}
for finder in finders.get_finders():
@@ -166,6 +197,6 @@ class WhiteNoiseMiddleware(WhiteNoise):
def get_static_url(self, name):
try:
- return decode_if_byte_string(staticfiles_storage.url(name))
+ return staticfiles_storage.url(name)
except ValueError:
return None
diff --git a/src/whitenoise/responders.py b/src/whitenoise/responders.py
index bd21ce8..9501ea6 100644
--- a/src/whitenoise/responders.py
+++ b/src/whitenoise/responders.py
@@ -4,7 +4,8 @@ import errno
import os
import re
import stat
-from email.utils import formatdate, parsedate
+from email.utils import formatdate
+from email.utils import parsedate
from http import HTTPStatus
from io import BufferedIOBase
from time import mktime
@@ -218,7 +219,7 @@ class StaticFile:
return False
last_requested_ts = parsedate(last_requested)
if last_requested_ts is not None:
- return parsedate(last_requested) >= self.last_modified
+ return last_requested_ts >= self.last_modified
return False
def get_path_and_headers(self, request_headers):
diff --git a/src/whitenoise/storage.py b/src/whitenoise/storage.py
index 919ce9b..029b0ff 100644
--- a/src/whitenoise/storage.py
+++ b/src/whitenoise/storage.py
@@ -4,118 +4,51 @@ import errno
import os
import re
import textwrap
+from typing import Any
+from typing import Iterator
+from typing import Tuple
+from typing import Union
from django.conf import settings
-from django.contrib.staticfiles.storage import (
- ManifestStaticFilesStorage,
- StaticFilesStorage,
-)
+from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
+from django.contrib.staticfiles.storage import StaticFilesStorage
from .compress import Compressor
+_PostProcessT = Iterator[Union[Tuple[str, str, bool], Tuple[str, None, RuntimeError]]]
-class CompressedStaticFilesMixin:
+
+class CompressedStaticFilesStorage(StaticFilesStorage):
"""
- Wraps a StaticFilesStorage instance to compress output files
+ StaticFilesStorage subclass that compresses output files.
"""
- def post_process(self, *args, **kwargs):
- super_post_process = getattr(
- super(),
- "post_process",
- self.fallback_post_process,
- )
- files = super_post_process(*args, **kwargs)
- if not kwargs.get("dry_run"):
- files = self.post_process_with_compression(files)
- return files
-
- # Only used if the class we're wrapping doesn't implement its own
- # `post_process` method
- def fallback_post_process(self, paths, dry_run=False, **options):
- if not dry_run:
- for path in paths:
- yield path, None, False
+ def post_process(
+ self, paths: dict[str, Any], dry_run: bool = False, **options: Any
+ ) -> _PostProcessT:
+ if dry_run:
+ return
- def create_compressor(self, **kwargs):
- return Compressor(**kwargs)
-
- def post_process_with_compression(self, files):
extensions = getattr(settings, "WHITENOISE_SKIP_COMPRESS_EXTENSIONS", None)
compressor = self.create_compressor(extensions=extensions, quiet=True)
- for name, hashed_name, processed in files:
- yield name, hashed_name, processed
- if isinstance(processed, Exception):
- continue
- unique_names = set(filter(None, [name, hashed_name]))
- for name in unique_names:
- if compressor.should_compress(name):
- path = self.path(name)
- prefix_len = len(path) - len(name)
- for compressed_path in compressor.compress(path):
- compressed_name = compressed_path[prefix_len:]
- yield name, compressed_name, True
-
-
-class CompressedStaticFilesStorage(CompressedStaticFilesMixin, StaticFilesStorage):
- pass
-
-
-class HelpfulExceptionMixin:
- """
- If a CSS file contains references to images, fonts etc that can't be found
- then Django's `post_process` blows up with a not particularly helpful
- ValueError that leads people to think WhiteNoise is broken.
-
- Here we attempt to intercept such errors and reformat them to be more
- helpful in revealing the source of the problem.
- """
-
- ERROR_MSG_RE = re.compile("^The file '(.+)' could not be found")
-
- ERROR_MSG = textwrap.dedent(
- """\
- {orig_message}
- The {ext} file '{filename}' references a file which could not be found:
- {missing}
-
- Please check the URL references in this {ext} file, particularly any
- relative paths which might be pointing to the wrong location.
- """
- )
-
- def post_process(self, *args, **kwargs):
- files = super().post_process(*args, **kwargs)
- for name, hashed_name, processed in files:
- if isinstance(processed, Exception):
- processed = self.make_helpful_exception(processed, name)
- yield name, hashed_name, processed
+ for path in paths:
+ if compressor.should_compress(path):
+ full_path = self.path(path)
+ prefix_len = len(full_path) - len(path)
+ for compressed_path in compressor.compress(full_path):
+ compressed_name = compressed_path[prefix_len:]
+ yield path, compressed_name, True
- def make_helpful_exception(self, exception, name):
- if isinstance(exception, ValueError):
- message = exception.args[0] if len(exception.args) else ""
- # Stringly typed exceptions. Yay!
- match = self.ERROR_MSG_RE.search(message)
- if match:
- extension = os.path.splitext(name)[1].lstrip(".").upper()
- message = self.ERROR_MSG.format(
- orig_message=message,
- filename=name,
- missing=match.group(1),
- ext=extension,
- )
- exception = MissingFileError(message)
- return exception
+ def create_compressor(self, **kwargs: Any) -> Compressor:
+ return Compressor(**kwargs)
class MissingFileError(ValueError):
pass
-class CompressedManifestStaticFilesStorage(
- HelpfulExceptionMixin, ManifestStaticFilesStorage
-):
+class CompressedManifestStaticFilesStorage(ManifestStaticFilesStorage):
"""
Extends ManifestStaticFilesStorage instance to create compressed versions
of its output files and, optionally, to delete the non-hashed files (i.e.
@@ -132,9 +65,15 @@ class CompressedManifestStaticFilesStorage(
def post_process(self, *args, **kwargs):
files = super().post_process(*args, **kwargs)
+
if not kwargs.get("dry_run"):
files = self.post_process_with_compression(files)
- return files
+
+ # Make exception messages helpful
+ for name, hashed_name, processed in files:
+ if isinstance(processed, Exception):
+ processed = self.make_helpful_exception(processed, name)
+ yield name, hashed_name, processed
def post_process_with_compression(self, files):
# Files may get hashed multiple times, we want to keep track of all the
@@ -199,3 +138,41 @@ class CompressedManifestStaticFilesStorage(
for compressed_path in compressor.compress(path):
compressed_name = compressed_path[prefix_len:]
yield name, compressed_name
+
+ def make_helpful_exception(self, exception, name):
+ """
+ If a CSS file contains references to images, fonts etc that can't be found
+ then Django's `post_process` blows up with a not particularly helpful
+ ValueError that leads people to think WhiteNoise is broken.
+
+ Here we attempt to intercept such errors and reformat them to be more
+ helpful in revealing the source of the problem.
+ """
+ if isinstance(exception, ValueError):
+ message = exception.args[0] if len(exception.args) else ""
+ # Stringly typed exceptions. Yay!
+ match = self._error_msg_re.search(message)
+ if match:
+ extension = os.path.splitext(name)[1].lstrip(".").upper()
+ message = self._error_msg.format(
+ orig_message=message,
+ filename=name,
+ missing=match.group(1),
+ ext=extension,
+ )
+ exception = MissingFileError(message)
+ return exception
+
+ _error_msg_re = re.compile(r"^The file '(.+)' could not be found")
+
+ _error_msg = textwrap.dedent(
+ """\
+ {orig_message}
+
+ The {ext} file '{filename}' references a file which could not be found:
+ {missing}
+
+ Please check the URL references in this {ext} file, particularly any
+ relative paths which might be pointing to the wrong location.
+ """
+ )
diff --git a/src/whitenoise/string_utils.py b/src/whitenoise/string_utils.py
index b56d262..6be9062 100644
--- a/src/whitenoise/string_utils.py
+++ b/src/whitenoise/string_utils.py
@@ -1,14 +1,6 @@
from __future__ import annotations
-def decode_if_byte_string(s, force_text=False):
- if isinstance(s, bytes):
- s = s.decode()
- if force_text and not isinstance(s, str):
- s = str(s)
- return s
-
-
# Follow Django in treating URLs as UTF-8 encoded (which requires undoing the
# implicit ISO-8859-1 decoding applied in Python 3). Strictly speaking, URLs
# should only be ASCII anyway, but UTF-8 can be found in the wild.
diff --git a/tests/django_settings.py b/tests/django_settings.py
index 7cb10fc..a571ef5 100644
--- a/tests/django_settings.py
+++ b/tests/django_settings.py
@@ -2,7 +2,10 @@ from __future__ import annotations
import os.path
-from .utils import TEST_FILE_PATH, AppServer
+import django
+
+from .utils import AppServer
+from .utils import TEST_FILE_PATH
ALLOWED_HOSTS = ["*"]
@@ -17,7 +20,14 @@ STATIC_URL = FORCE_SCRIPT_NAME + "/static/"
STATIC_ROOT = os.path.join(TEST_FILE_PATH, "root")
-STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
+if django.VERSION >= (4, 2):
+ STORAGES = {
+ "staticfiles": {
+ "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
+ },
+ }
+else:
+ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
MIDDLEWARE = ["whitenoise.middleware.WhiteNoiseMiddleware"]
diff --git a/tests/test_django_whitenoise.py b/tests/test_django_whitenoise.py
index c35e541..2654424 100644
--- a/tests/test_django_whitenoise.py
+++ b/tests/test_django_whitenoise.py
@@ -3,20 +3,22 @@ from __future__ import annotations
import shutil
import tempfile
from contextlib import closing
-from urllib.parse import urljoin, urlparse
+from urllib.parse import urljoin
+from urllib.parse import urlparse
-import django
import pytest
from django.conf import settings
-from django.contrib.staticfiles import finders, storage
+from django.contrib.staticfiles import finders
+from django.contrib.staticfiles import storage
from django.core.management import call_command
from django.core.wsgi import get_wsgi_application
from django.test.utils import override_settings
from django.utils.functional import empty
-from whitenoise.middleware import WhiteNoiseFileResponse, WhiteNoiseMiddleware
-
-from .utils import AppServer, Files
+from .utils import AppServer
+from .utils import Files
+from whitenoise.middleware import WhiteNoiseFileResponse
+from whitenoise.middleware import WhiteNoiseMiddleware
def reset_lazy_object(obj):
@@ -86,9 +88,7 @@ def test_unversioned_file_not_cached_forever(server, static_files, _collect_stat
url = settings.STATIC_URL + static_files.js_path
response = server.get(url)
assert response.content == static_files.js_content
- assert response.headers.get("Cache-Control") == "max-age={}, public".format(
- WhiteNoiseMiddleware.max_age
- )
+ assert response.headers.get("Cache-Control") == "max-age=60, public"
def test_get_gzip(server, static_files, _collect_static):
@@ -204,7 +204,6 @@ def test_whitenoise_file_response_has_only_one_header():
assert headers == {"content-type"}
-@pytest.mark.skipif(django.VERSION[:2] < (3, 1), reason="feature added in Django 3.1")
def test_relative_static_url(server, static_files, _collect_static):
with override_settings(STATIC_URL="static/"):
url = storage.staticfiles_storage.url(static_files.js_path)
diff --git a/tests/test_runserver_nostatic.py b/tests/test_runserver_nostatic.py
index 4614cfd..65ed458 100644
--- a/tests/test_runserver_nostatic.py
+++ b/tests/test_runserver_nostatic.py
@@ -1,6 +1,7 @@
from __future__ import annotations
-from django.core.management import get_commands, load_command_class
+from django.core.management import get_commands
+from django.core.management import load_command_class
def get_command_instance(name):
diff --git a/tests/test_storage.py b/tests/test_storage.py
index beeae9b..fca9a60 100644
--- a/tests/test_storage.py
+++ b/tests/test_storage.py
@@ -6,20 +6,18 @@ import shutil
import tempfile
from posixpath import basename
+import django
import pytest
from django.conf import settings
-from django.contrib.staticfiles.storage import HashedFilesMixin, staticfiles_storage
+from django.contrib.staticfiles.storage import HashedFilesMixin
+from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.management import call_command
from django.test.utils import override_settings
from django.utils.functional import empty
-from whitenoise.storage import (
- CompressedManifestStaticFilesStorage,
- HelpfulExceptionMixin,
- MissingFileError,
-)
-
from .utils import Files
+from whitenoise.storage import CompressedManifestStaticFilesStorage
+from whitenoise.storage import MissingFileError
@pytest.fixture()
@@ -38,27 +36,54 @@ def setup():
@pytest.fixture()
def _compressed_storage(setup):
- with override_settings(
- STATICFILES_STORAGE="whitenoise.storage.CompressedStaticFilesStorage"
- ):
- call_command("collectstatic", verbosity=0, interactive=False)
+ backend = "whitenoise.storage.CompressedStaticFilesStorage"
+ if django.VERSION >= (4, 2):
+ storages = {
+ "STORAGES": {
+ **settings.STORAGES,
+ "staticfiles": {"BACKEND": backend},
+ }
+ }
+ else:
+ storages = {"STATICFILES_STORAGE": backend}
+
+ with override_settings(**storages):
+ yield
@pytest.fixture()
def _compressed_manifest_storage(setup):
- with override_settings(
- STATICFILES_STORAGE="whitenoise.storage.CompressedManifestStaticFilesStorage",
- WHITENOISE_KEEP_ONLY_HASHED_FILES=True,
- ):
+ backend = "whitenoise.storage.CompressedManifestStaticFilesStorage"
+ if django.VERSION >= (4, 2):
+ storages = {
+ "STORAGES": {
+ **settings.STORAGES,
+ "staticfiles": {"BACKEND": backend},
+ }
+ }
+ else:
+ storages = {"STATICFILES_STORAGE": backend}
+
+ with override_settings(**storages, WHITENOISE_KEEP_ONLY_HASHED_FILES=True):
call_command("collectstatic", verbosity=0, interactive=False)
-def test_compressed_files_are_created(_compressed_storage):
+def test_compressed_static_files_storage(_compressed_storage):
+ call_command("collectstatic", verbosity=0, interactive=False)
+
for name in ["styles.css.gz", "styles.css.br"]:
path = os.path.join(settings.STATIC_ROOT, name)
assert os.path.exists(path)
+def test_compressed_static_files_storage_dry_run(_compressed_storage):
+ call_command("collectstatic", "--dry-run", verbosity=0, interactive=False)
+
+ for name in ["styles.css.gz", "styles.css.br"]:
+ path = os.path.join(settings.STATIC_ROOT, name)
+ assert not os.path.exists(path)
+
+
def test_make_helpful_exception(_compressed_manifest_storage):
class TriggerException(HashedFilesMixin):
def exists(self, path):
@@ -69,7 +94,7 @@ def test_make_helpful_exception(_compressed_manifest_storage):
TriggerException().hashed_name("/missing/file.png")
except ValueError as e:
exception = e
- helpful_exception = HelpfulExceptionMixin().make_helpful_exception(
+ helpful_exception = CompressedManifestStaticFilesStorage().make_helpful_exception(
exception, "styles/app.css"
)
assert isinstance(helpful_exception, MissingFileError)
diff --git a/tests/test_string_utils.py b/tests/test_string_utils.py
index 9080457..3d56524 100644
--- a/tests/test_string_utils.py
+++ b/tests/test_string_utils.py
@@ -1,21 +1,6 @@
from __future__ import annotations
-from whitenoise.string_utils import decode_if_byte_string, ensure_leading_trailing_slash
-
-
-class DecodeIfByteStringTests:
- def test_bytes(self):
- assert decode_if_byte_string(b"abc") == "abc"
-
- def test_unforced(self):
- x = object()
- assert decode_if_byte_string(x) is x
-
- def test_forced(self):
- x = object()
- result = decode_if_byte_string(x, force_text=True)
- assert isinstance(result, str)
- assert result.startswith("<object object at ")
+from whitenoise.string_utils import ensure_leading_trailing_slash
class EnsureLeadingTrailingSlashTests:
diff --git a/tests/test_whitenoise.py b/tests/test_whitenoise.py
index 9628736..3e98f03 100644
--- a/tests/test_whitenoise.py
+++ b/tests/test_whitenoise.py
@@ -14,11 +14,11 @@ from wsgiref.simple_server import demo_app
import pytest
+from .utils import AppServer
+from .utils import Files
from whitenoise import WhiteNoise
from whitenoise.responders import StaticFile
-from .utils import AppServer, Files
-
@pytest.fixture(scope="module")
def files():
@@ -71,13 +71,6 @@ def server(application):
yield app_server
-def test_invalid_kwarg():
- with pytest.raises(TypeError) as excinfo:
- WhiteNoise(None, invalid=True)
-
- assert excinfo.value.args == ("Unexpected keyword argument 'invalid'",)
-
-
def assert_is_default_response(response):
assert "Hello world!" in response.text
@@ -342,13 +335,27 @@ def test_directory_path_can_be_pathlib_instance():
WhiteNoise(None, root=root, autorefresh=True)
-def test_last_modified_not_set_when_mtime_is_zero():
- class FakeStatEntry:
- st_mtime = 0
- st_size = 1024
- st_mode = stat.S_IFREG
+def fake_stat_entry(
+ st_mode: int = stat.S_IFREG, st_size: int = 1024, st_mtime: int = 0
+) -> os.stat_result:
+ return os.stat_result(
+ (
+ st_mode,
+ 0, # st_ino
+ 0, # st_dev
+ 0, # st_nlink
+ 0, # st_uid
+ 0, # st_gid
+ st_size,
+ 0, # st_atime
+ st_mtime,
+ 0, # st_ctime
+ )
+ )
+
- stat_cache = {__file__: FakeStatEntry()}
+def test_last_modified_not_set_when_mtime_is_zero():
+ stat_cache = {__file__: fake_stat_entry()}
responder = StaticFile(__file__, [], stat_cache=stat_cache)
response = responder.get_response("GET", {})
response.file.close()
@@ -358,12 +365,7 @@ def test_last_modified_not_set_when_mtime_is_zero():
def test_file_size_matches_range_with_range_header():
- class FakeStatEntry:
- st_mtime = 0
- st_size = 1024
- st_mode = stat.S_IFREG
-
- stat_cache = {__file__: FakeStatEntry()}
+ stat_cache = {__file__: fake_stat_entry()}
responder = StaticFile(__file__, [], stat_cache=stat_cache)
response = responder.get_response("GET", {"HTTP_RANGE": "bytes=0-13"})
file_size = len(response.file.read())
@@ -371,15 +373,11 @@ def test_file_size_matches_range_with_range_header():
def test_chunked_file_size_matches_range_with_range_header():
- class FakeStatEntry:
- st_mtime = 0
- st_size = 1024
- st_mode = stat.S_IFREG
-
- stat_cache = {__file__: FakeStatEntry()}
+ stat_cache = {__file__: fake_stat_entry()}
responder = StaticFile(__file__, [], stat_cache=stat_cache)
response = responder.get_response("GET", {"HTTP_RANGE": "bytes=0-13"})
file_size = 0
+ assert response.file is not None
while response.file.read(1):
file_size += 1
assert file_size == 14
diff --git a/tests/utils.py b/tests/utils.py
index e241b31..0db45d7 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -2,15 +2,12 @@ from __future__ import annotations
import os
import threading
-import warnings
-from wsgiref.simple_server import WSGIRequestHandler, make_server
+from wsgiref.simple_server import make_server
+from wsgiref.simple_server import WSGIRequestHandler
from wsgiref.util import shift_path_info
import requests
-warnings.filterwarnings(action="ignore", category=DeprecationWarning, module="requests")
-
-
TEST_FILE_PATH = os.path.join(os.path.dirname(__file__), "test_files")
diff --git a/tox.ini b/tox.ini
index 61f9af5..3591026 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,24 +1,27 @@
[tox]
envlist =
- py37-django{22,30,31,32}
- py38-django{22,30,31,32,40}
- py39-django{22,30,31,32,40}
- py310-django{32,40}
- py310-codegen
+ py37-django{32}
+ py38-django{32,40,41,42}
+ py39-django{32,40,41,42}
+ py310-django{32,40,41,42}
+ py311-django{41,42}
+ py311-codegen
[testenv]
+# Ignoring 'path is deprecated' from certifi: https://github.com/certifi/python-certifi/issues/192
commands =
python \
-W error::ResourceWarning \
-W error::DeprecationWarning \
-W error::PendingDeprecationWarning \
+ -W 'ignore:path is deprecated. Use files() instead.:DeprecationWarning' \
-m coverage run \
-m pytest {posargs:tests}
deps = -r requirements/{envname}.txt
setenv =
- PYTHONDEVMODE=1
+ PYTHONDEVMODE=1
-[testenv:py310-codegen]
+[testenv:py311-codegen]
commands =
python ./scripts/generate_default_media_types.py --check
deps =