pax_global_header00006660000000000000000000000064142010365670014516gustar00rootroot0000000000000052 comment=c82f9a3606074d53063243308c2a9afdbe0195cb whitenoise-6.0.0/000077500000000000000000000000001420103656700136775ustar00rootroot00000000000000whitenoise-6.0.0/.editorconfig000066400000000000000000000003451420103656700163560ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.py] indent_size = 4 [Makefile] indent_style = tab whitenoise-6.0.0/.gitattributes000066400000000000000000000000231420103656700165650ustar00rootroot00000000000000* text=auto eol=lf whitenoise-6.0.0/.github/000077500000000000000000000000001420103656700152375ustar00rootroot00000000000000whitenoise-6.0.0/.github/workflows/000077500000000000000000000000001420103656700172745ustar00rootroot00000000000000whitenoise-6.0.0/.github/workflows/main.yml000066400000000000000000000035511420103656700207470ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: tests: name: Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: - ubuntu-20.04 - windows-2022 python-version: - "3.7" - "3.8" - "3.9" - "3.10" steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: 'requirements/*.txt' - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel python -m pip install --upgrade tox tox-py - name: Run tox targets for ${{ matrix.python-version }} run: tox --py current - name: Upload coverage data if: matrix.os != 'windows-2022' uses: actions/upload-artifact@v2 with: name: coverage-data path: '.coverage.*' coverage: name: Coverage runs-on: ubuntu-20.04 needs: tests steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: '3.10' - name: Install dependencies run: python -m pip install --upgrade coverage[toml] - name: Download data uses: actions/download-artifact@v2 with: name: coverage-data - name: Combine coverage and fail if it's <100% run: | python -m coverage combine python -m coverage html --skip-covered --skip-empty python -m coverage report --fail-under=95 - name: Upload HTML report if: ${{ failure() }} uses: actions/upload-artifact@v2 with: name: html-report path: htmlcov whitenoise-6.0.0/.gitignore000066400000000000000000000001511420103656700156640ustar00rootroot00000000000000*.py[co] __pycache__ /MANIFEST /.tox /.coverage /.coverage.* /htmlcov /docs/_build /dist /src/*.egg-info whitenoise-6.0.0/.pre-commit-config.yaml000066400000000000000000000020701420103656700201570ustar00rootroot00000000000000default_language_version: python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-json - id: check-merge-conflict - id: check-symlinks - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade rev: v2.31.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black rev: 22.1.0 hooks: - id: black - repo: https://github.com/asottile/blacken-docs rev: v1.12.1 hooks: - id: blacken-docs additional_dependencies: - black==22.1.0 - repo: https://github.com/pycqa/isort rev: 5.10.1 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - 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] whitenoise-6.0.0/.readthedocs.yaml000066400000000000000000000004431420103656700171270ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-20.04 tools: python: "3.10" sphinx: configuration: docs/conf.py formats: all python: install: - requirements: docs/requirements.txt whitenoise-6.0.0/CHANGELOG.rst000066400000000000000000000001551420103656700157210ustar00rootroot00000000000000Changelog ========= Please see the `documentation `_. whitenoise-6.0.0/LICENSE000066400000000000000000000020661420103656700147100ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 David Evans Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. whitenoise-6.0.0/MANIFEST.in000066400000000000000000000004231420103656700154340ustar00rootroot00000000000000global-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 whitenoise-6.0.0/README.rst000066400000000000000000000037161420103656700153750ustar00rootroot00000000000000WhiteNoise ========== .. 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 own static files, making it a self-contained unit that can be deployed anywhere without relying on nginx, Amazon S3 or any other external service. (Especially useful on Heroku, OpenShift and other PaaS providers.) It's designed to work nicely with a CDN for high-traffic sites so you don't have to sacrifice performance to benefit from simplicity. WhiteNoise works with any WSGI-compatible app but has some special auto-configuration features for Django. WhiteNoise takes care of best-practices for you, for instance: * Serving compressed content (gzip and Brotli formats, handling Accept-Encoding and Vary headers correctly) * Setting far-future cache headers on content which won't change Worried that serving static files with Python is horribly inefficient? Still think you should be using Amazon S3? Have a look at the `Infrequently Asked Questions`_. To get started, see the documentation_. .. _Infrequently Asked Questions: https://whitenoise.evans.io/en/stable/#infrequently-asked-questions .. _documentation: https://whitenoise.evans.io/en/stable/ whitenoise-6.0.0/docs/000077500000000000000000000000001420103656700146275ustar00rootroot00000000000000whitenoise-6.0.0/docs/Makefile000066400000000000000000000127141420103656700162740ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WhiteNoise.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WhiteNoise.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/WhiteNoise" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WhiteNoise" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." whitenoise-6.0.0/docs/base.rst000066400000000000000000000246501420103656700163020ustar00rootroot00000000000000Using WhiteNoise with any WSGI application ========================================== .. note:: These instructions apply to any WSGI application. However, for Django applications you would be better off using the :doc:`WhiteNoiseMiddleware ` class which makes integration easier. To enable WhiteNoise you need to wrap your existing WSGI application in a WhiteNoise instance and tell it where to find your static files. For example: .. code-block:: python from whitenoise import WhiteNoise from my_project import MyWSGIApp application = MyWSGIApp() application = WhiteNoise(application, root="/path/to/static/files") application.add_files("/path/to/more/static/files", prefix="more-files/") On initialization, WhiteNoise walks over all the files in the directories that have been added (descending into sub-directories) and builds a list of available static files. Any requests which match a static file get served by WhiteNoise, all others are passed through to the original WSGI application. See the sections on :ref:`compression ` and :ref:`caching ` for further details. WhiteNoise API -------------- .. class:: WhiteNoise(application, root=None, prefix=None, \**kwargs) :param callable application: Original WSGI application :param str root: If set, passed to ``add_files`` method :param str prefix: If set, passed to ``add_files`` method :param \**kwargs: Sets :ref:`configuration attributes ` for this instance .. method:: WhiteNoise.add_files(root, prefix=None) :param str root: Absolute path to a directory of static files to be served :param str prefix: If set, the URL prefix under which the files will be served. Trailing slashes are automatically added. .. _compression: Compression Support ------------------- When WhiteNoise builds its list of available files it checks for corresponding files with a ``.gz`` and a ``.br`` suffix (e.g., ``scripts/app.js``, ``scripts/app.js.gz`` and ``scripts/app.js.br``). If it finds them, it will assume that they are (respectively) gzip and `brotli `_ compressed versions of the original file and it will serve them in preference to the uncompressed version where clients indicate that they that compression format (see note on Amazon S3 for why this behaviour is important). .. _cli-utility: WhiteNoise comes with a command line utility which will generate compressed versions of your files for you. Note that in order for brotli compression to work the `Brotli Python package `_ must be installed. Usage is simple: .. code-block:: console $ python -m whitenoise.compress --help usage: compress.py [-h] [-q] [--no-gzip] [--no-brotli] root [extensions [extensions ...]] Search for all files inside *not* matching and produce compressed versions with '.gz' and '.br' suffixes (as long as this results in a smaller file) positional arguments: root Path root from which to search for files extensions File extensions to exclude from compression (default: jpg, jpeg, png, gif, webp, zip, gz, tgz, bz2, tbz, xz, br, swf, flv, woff, woff2) optional arguments: -h, --help show this help message and exit -q, --quiet Don't produce log output --no-gzip Don't produce gzip '.gz' files --no-brotli Don't produce brotli '.br' files You can either run this during development and commit your compressed files to your repository, or you can run this as part of your build and deploy processes. (Note that this is handled automatically in Django if you're using the custom storage backend.) .. _caching: Caching Headers --------------- By default, WhiteNoise sets a max-age header on all responses it sends. You can configure this by passing a ``max_age`` keyword argument. WhiteNoise sets both ``Last-Modified`` and ``ETag`` headers for all files and will return Not Modified responses where appropriate. The ETag header uses the same format as nginx which is based on the size and last-modified time of the file. If you want to use a different scheme for generating ETags you can set them via you own function by using the :any:`add_headers_function` option. Most modern static asset build systems create uniquely named versions of each file. This results in files which are immutable (i.e., they can never change their contents) and can therefore by cached indefinitely. In order to take advantage of this, WhiteNoise needs to know which files are immutable. This can be done using the :any:`immutable_file_test` option which accepts a reference to a function. The exact details of how you implement this method will depend on your particular asset build system but see the :any:`option documentation ` for a simple example. Once you have implemented this, any files which are flagged as immutable will have "cache forever" headers set. .. _index_files: Index Files ----------- When the :any:`index_file` option is enabled: * Visiting ``/example/`` will serve the file at ``/example/index.html`` * Visiting ``/example`` will redirect (302) to ``/example/`` * Visiting ``/example/index.html`` will redirect (302) to ``/example/`` If you want to something other than ``index.html`` as the index file, then you can also set this option to an alternative filename. Using a Content Distribution Network ------------------------------------ See the instructions for :ref:`using a CDN with Django ` . The same principles apply here although obviously the exact method for generating the URLs for your static files will depend on the libraries you're using. Redirecting to HTTPS -------------------- WhiteNoise does not handle redirection itself, but works well alongside `wsgi-sslify`_, which performs HTTP to HTTPS redirection as well as optionally setting an HSTS header. Simply wrap the WhiteNoise WSGI application with ``sslify()`` - see the `wsgi-sslify`_ documentation for more details. .. _wsgi-sslify: https://github.com/jacobian/wsgi-sslify .. _configuration: Configuration attributes ------------------------ These can be set by passing keyword arguments to the constructor, or by sub-classing WhiteNoise and setting the attributes directly. .. attribute:: autorefresh :default: ``False`` Recheck the filesystem to see if any files have changed before responding. This is designed to be used in development where it can be convenient to pick up changes to static files without restarting the server. For both performance and security reasons, this setting should not be used in production. .. attribute:: max_age :default: ``60`` Time (in seconds) for which browsers and proxies should cache files. The default is chosen to be short enough not to cause problems with stale versions but 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. .. attribute:: index_file :default: ``False`` If ``True`` enable :ref:`index file serving `. If set to a non-empty string, enable index files and use that string as the index file name. .. attribute:: mimetypes :default: ``None`` A dictionary mapping file extensions (lowercase) to the mimetype for that extension. For example: :: {'.foo': 'application/x-foo'} 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 ` file. In addition to file extensions, mimetypes can be specified by supplying the entire filename, for example: :: {'some-special-file': 'application/x-custom-type'} .. attribute:: charset :default: ``utf-8`` Charset to add as part of the ``Content-Type`` header for all files whose mimetype allows a charset. .. attribute:: allow_all_origins :default: ``True`` Toggles whether to send an ``Access-Control-Allow-Origin: *`` header for all static files. This allows cross-origin requests for static files which means your static files will continue to work as expected even if they are served via a CDN and therefore on a different domain. Without this your static files will *mostly* work, but you may have problems with fonts loading in Firefox, or accessing images in canvas elements, or other mysterious things. The W3C `explicitly state`__ that this behaviour is safe for publicly accessible files. .. __: https://www.w3.org/TR/cors/#security .. attribute:: add_headers_function :default: ``None`` Reference to a function which is passed the headers object for each static file, allowing it to modify them. For example: :: def force_download_pdfs(headers, path, url): if path.endswith('.pdf'): headers['Content-Disposition'] = 'attachment' application = WhiteNoise(application, add_headers_function=force_download_pdfs) The function is passed: headers A `wsgiref.headers`__ instance (which you can treat just as a dict) containing the headers for the current file path The absolute path to the local file url The host-relative URL of the file e.g. ``/static/styles/app.css`` The function should not return anything; changes should be made by modifying the headers dictionary directly. .. __: https://docs.python.org/3/library/wsgiref.html#module-wsgiref.headers .. attribute:: immutable_file_test :default: ``return False`` Reference to function, or string. If a reference to a function, this is passed the path and URL for each static file and should return whether that file is immutable, i.e. guaranteed not to change, and so can be safely cached forever. If a string, this is treated as a regular expression and each file's URL is matched against it. Example: :: def immutable_file_test(path, url): # Match filename with 12 hex digits before the extension # e.g. app.db8f2edc0c8a.js return re.match(r'^.+\.[0-9a-f]{12}\..+$', url) The function is passed: path The absolute path to the local file url The host-relative URL of the file e.g. ``/static/styles/app.css`` whitenoise-6.0.0/docs/changelog.rst000066400000000000000000000441541420103656700173200ustar00rootroot00000000000000Changelog ========= 6.0.0 ----- * Drop support for Python 3.5 and 3.6. * Add support for Python 3.9 and 3.10. * Drop support for Django 1.11, 2.0, and 2.1. * Add support for Django 4.0. * Import new MIME types from Nginx, changes: - ``.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. * 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. Thanks to Richard Tibbles in `PR #322 `__. * Treat empty and ``"*"`` values for ``Accept-Encoding`` as if the client doesn’t support any encoding. Thanks to Richard Tibbles in `PR #323 `__. 5.3.0 (2021-07-16) ------------------ * Gracefully handle unparsable If-Modified-Since headers (thanks `@danielegozzi `_). * Test against Django 3.2 (thanks `@jhnbkr `_). * Add mimetype for Markdown (``.md``) files (thanks `@bz2 `_). * Various documentation improvements (thanks `@PeterJCLaw `_ and `@AliRn76 `_). 5.2.0 (2020-08-04) ------------------ * Add support for `relative STATIC_URLs `_ 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 `_). * Various documentation improvements (thanks `@lukeburden `_). 5.1.0 (2020-05-20) ------------------ * Add a :any:`manifest_strict ` setting to prevent Django throwing errors when missing files are referenced (thanks `@MegacoderKim `_). 5.0.1 (2019-12-12) ------------------ * Fix packaging to indicate only Python 3.5+ compatibiity (thanks `@mdalp `_). 5.0 (2019-12-10) ---------------- .. note:: This is a major version bump, but only because it removes Python 2 compatibility. If you were already running under Python 3 then there should be no breaking changes. WhiteNoise is now tested on Python 3.5--3.8 and Django 2.0--3.0. Other changes include: * Fix incompatibility with Django 3.0 which caused problems with Safari (details `here `_). Thanks `@paltman `_ and `@giilby `_ for diagnosing. * Lots of improvements to the test suite (including switching to py.test). Thanks `@NDevox `_ and `@Djailla `_. 4.1.4 (2019-09-24) ------------------ * Make tests more deterministic and easier to run outside of ``tox``. * Fix Fedora packaging `issue `_. * Use `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 `_ for reporting). * Fix potential path traversal attack while running in autorefresh mode on Windows (thanks `@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 `_ ). * Stop accessing the FILE_CHARSET Django setting which was almost entirely unused and is now deprecated (thanks `@timgraham `_). 4.1.1 (2018-11-12) ------------------ * Fix `bug `_ in ETag handling (thanks `@edmorley `_). * Documentation fixes (thanks `@jamesbeith `_ and `@mathieusteele `_). 4.1 (2018-09-12) ---------------- * Silenced spurious warning about missing directories when in development (i.e "autorefresh") mode. * Support supplying paths as `Pathlib `_ instances, rather than just strings (thanks `@browniebroke `_). * Add a new :ref:`CompressedStaticFilesStorage ` backend to support applying compression without applying Django's hash-versioning process. * Documentation improvements. 4.0 (2018-08-10) ---------------- .. note:: **Breaking changes** The latest version of WhiteNoise removes some options which were deprecated in the previous major release: * The WSGI integration option for Django (which involved editing ``wsgi.py``) has been removed. Instead, you should add WhiteNoise to your middleware list in ``settings.py`` and remove any reference to WhiteNoise from ``wsgi.py``. See the :ref:`documentation ` for more details. (The :doc:`pure WSGI ` integration is still available for non-Django apps.) * The ``whitenoise.django.GzipManifestStaticFilesStorage`` alias has now been removed. Instead you should use the correct import path: ``whitenoise.storage.CompressedManifestStaticFilesStorage``. If you are not using either of these integration options you should have no issues upgrading to the latest version. .. rubric:: Removed Python 3.3 Support Removed support for Python 3.3 since it's end of life was in September 2017. .. rubric:: Index file support WhiteNoise now supports serving :ref:`index files ` for directories (e.g. serving ``/example/index.html`` at ``/example/``). It also creates redirects so that visiting the index file directly, or visiting the URL without a trailing slash will redirect to the correct URL. .. rubric:: Range header support ("byte serving") WhiteNoise now respects the HTTP Range header which allows a client to request only part of a file. The main use for this is in serving video files to iOS devices as Safari refuses to play videos unless the server supports the Range header. .. rubric:: ETag support WhiteNoise now adds ETag headers to files using the same algorithm used by nginx. This gives slightly better caching behaviour than relying purely on Last Modified dates (although not as good as creating immutable files using something like ``ManifestStaticFilesStorage``, which is still the best option if you can use it). If you need to generate your own ETags headers for any reason you can define a custom :any:`add_headers_function `. .. rubric:: Remove requirement to run collectstatic By setting :any:`WHITENOISE_USE_FINDERS` to ``True`` files will be served directly from their original locations (usually in ``STATICFILES_DIRS`` or app ``static`` subdirectories) without needing to be collected into ``STATIC_ROOT`` by the collectstatic command. This was always the default behaviour when in ``DEBUG`` mode but previously it wasn't possible to enable this behaviour in production. For small apps which aren't using the caching and compression features of the more advanced storage backends this simplifies the deployment process by removing the need to run collectstatic as part of the build step -- in fact, it's now possible not to have any build step at all. .. rubric:: Customisable immutable files test WhiteNoise ships with code which detects when you are using Django's ManifestStaticFilesStorage backend and sends optimal caching headers for files which are guaranteed not to change. If you are using a different system for generating cacheable files then you might need to supply your own function for detecting such files. Previously this required subclassing WhiteNoise, but now you can use the :any:`WHITENOISE_IMMUTABLE_FILE_TEST` setting. .. rubric:: Fix runserver_nostatic to work with Channels The old implementation of :ref:`runserver_nostatic ` (which disables Django's default static file handling in development) did not work with `Channels`_, which needs its own runserver implementation. The runserver_nostatic command has now been rewritten so that it should work with Channels and with any other app which provides its own runserver. .. _Channels: https://channels.readthedocs.io/ .. rubric:: Reduced storage requirements for static files The new :any:`WHITENOISE_KEEP_ONLY_HASHED_FILES` setting reduces the number of files in STATIC_ROOT by half by storing files only under their hashed names (e.g. ``app.db8f2edc0c8a.js``), rather than also keeping a copy with the original name (e.g. ``app.js``). .. rubric:: Improved start up performance When in production mode (i.e. when :any:`autorefresh ` is disabled), WhiteNoise scans all static files when the application starts in order to be able to serve them as efficiently and securely as possible. For most applications this makes no noticeable difference to start up time, however for applications with very large numbers of static files this process can take some time. In WhiteNoise 4.0 the file scanning code has been rewritten to do the minimum possible amount of filesystem access which should make the start up process considerably faster. .. rubric:: Windows Testing WhiteNoise has always aimed to support Windows as well as \*NIX platforms but we are now able to run the test suite against Windows as part of the CI process which should ensure that we can maintain Windows compatibility in future. .. rubric:: Modification times for compressed files The compressed storage backend (which generates Gzip and Brotli compressed files) now ensures that compressed files have the same modification time as the originals. This only makes a difference if you are using the compression backend with something other than WhiteNoise to actually serve the files, which very few users do. .. rubric:: Replaced brotlipy with official Brotli Python Package Since the official `Brotli project `_ offers a `Brotli Python package `_ brotlipy has been replaced with Brotli. Furthermore a ``brotli`` key has been added to ``extras_require`` which allows installing WhiteNoise and Brotli together like this: .. code-block:: bash pip install whitenoise[brotli] 3.3.1 (2017-09-23) ------------------ * 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 `_ 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. 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. 3.2 (2016-05-27) ---------------- * 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 `_). * Return Vary and Cache-Control headers on 304 responses, as specified by the `RFC `_. 3.1 (2016-05-15) ---------------- * 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) ---------------- .. note:: The latest version of WhiteNoise contains some small **breaking changes**. Most users will be able to upgrade without any problems, but some less-used APIs have been modified: * The setting ``WHITENOISE_GZIP_EXCLUDE_EXTENSIONS`` has been renamed to ``WHITENOISE_SKIP_COMPRESS_EXTENSIONS``. * The CLI :ref:`compression 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 ` 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 import path **will continue to work** for now, but users are encouraged to update their code to use the new path. .. _WhiteNoise 2.x: https://whitenoise.evans.io/en/legacy-2.x/ .. rubric:: Simpler, cleaner Django middleware integration WhiteNoise can now integrate with Django by adding a single line to ``MIDDLEWARE_CLASSES`` without any need to edit ``wsgi.py``. This also means that WhiteNoise plays nicely with other middleware classes such as *SecurityMiddleware*, and that it is fully compatible with the new `Channels`_ system. See the :ref:`updated documentation ` for details. .. _Channels: https://channels.readthedocs.io/ .. rubric:: Brotli compression support `Brotli`_ is the modern, more efficient alternative to gzip for HTTP compression. To benefit from smaller files and faster page loads, just install the `brotlipy`_ library, update your ``requirements.txt`` and WhiteNoise will take care of the rest. See the :ref:`documentation ` for details. .. _brotli: https://en.wikipedia.org/wiki/Brotli .. _brotlipy: https://brotlipy.readthedocs.io/ .. rubric:: Simpler customisation It's now possible to add custom headers to WhiteNoise without needing to create a subclass, using the new :any:`add_headers_function ` setting. .. rubric:: Use WhiteNoise in development with Django There's now an option to force Django to use WhiteNoise in development, rather than its own static file handling. This results in more consistent behaviour between development and production environments and fewer opportunities for bugs and surprises. See the :ref:`documentation ` for details. .. rubric:: Improved mimetype handling WhiteNoise now ships with its own mimetype definitions (based on those shipped with nginx) instead of relying on the system ones, which can vary between environments. There is a new :any:`mimetypes ` configuration option which makes it easy to add additional type definitions if needed. .. rubric:: Thanks A big thank-you to `Ed Morley `_ and `Tim Graham `_ for their contributions to this release. 2.0.6 (2015-11-15) ------------------ * Rebuild with latest version of `wheel` to get `extras_require` support. 2.0.5 (2015-11-15) ------------------ * Add missing argparse dependency for Python 2.6 (thanks @movermeyer)). 2.0.4 (2015-09-20) ------------------ * Report path on MissingFileError (thanks @ezheidtmann). 2.0.3 (2015-08-18) ------------------ * 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). 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. 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. * 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) ------------------ * Fix unhelpful exception inside `make_helpful_exception` on Python 3 (thanks @abbottc). 1.0.5 (2014-11-25) ------------------ * Fix error when attempting to gzip empty files (thanks @ryanrhee). 1.0.4 (2014-11-14) ------------------ * 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) ------------------ * Fix bug in Last Modified date handling (thanks to Atsushi Odagiri for spotting). 1.0.2 (2014-04-29) ------------------ * Set the default max_age parameter in base class to be what the docs claimed it was. 1.0.1 (2014-04-18) ------------------ * Fix path-to-URL conversion for Windows. * Remove cruft from packaging manifest. 1.0 (2014-04-14) ---------------- * First stable release. whitenoise-6.0.0/docs/conf.py000066400000000000000000000211221420103656700161240ustar00rootroot00000000000000# 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 import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # Get the project root dir, which is the parent dir of this cwd = os.getcwd() project_root = os.path.dirname(cwd) # Insert the project root dir as the first element in the PYTHONPATH. # This lets us ensure that the source package is imported, and that its # version is used. 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 = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "WhiteNoise" copyright = f"2013-{datetime.datetime.today().year}, David Evans" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. def _get_version() -> str: with open(os.path.join(project_root, "setup.cfg")) as setup_fp: version_lines = [ line.strip() for line in setup_fp if line.startswith("version = ") ] assert len(version_lines) == 1 return version_lines[0].split(" = ")[1] 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 = [ "_build", "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 # " v 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 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 # 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 # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "WhiteNoise", "WhiteNoise Documentation", "David Evans", "WhiteNoise", "One line description of project.", "Miscellaneous", ) ] # 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", "")} whitenoise-6.0.0/docs/django.rst000066400000000000000000000670661420103656700166420ustar00rootroot00000000000000Using WhiteNoise with Django ============================ .. note:: To use WhiteNoise with a non-Django application see the :doc:`generic WSGI documentation `. This guide walks you through setting up a Django project with WhiteNoise. In most cases it shouldn't take more than a couple of lines of configuration. I mention Heroku in a few places as that was the initial use case which prompted me to create WhiteNoise, but there's nothing Heroku-specific about WhiteNoise and the instructions below should apply whatever your hosting platform. 1. Make sure *staticfiles* is configured correctly ---------------------------------------------------- If you're familiar with Django you'll know what to do. If you're just getting started with a new Django project then you'll need add the following to the bottom of your ``settings.py`` file: .. code-block:: python STATIC_ROOT = BASE_DIR / "staticfiles" As part of deploying your application you'll need to run ``./manage.py collectstatic`` to put all your static files into ``STATIC_ROOT``. (If you're running on Heroku then this is done automatically for you.) Make sure you're using the static_ template tag to refer to your static files, rather than writing the URL directly. For example: .. code-block:: django {% load static %} Hi! Hi! For further details see the Django `staticfiles `_ guide. .. _static: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#std:templatetag-static .. _django-middleware: 2. Enable WhiteNoise -------------------- Edit your ``settings.py`` file and add WhiteNoise to the ``MIDDLEWARE`` list. The WhiteNoise middleware should be placed directly after the Django `SecurityMiddleware `_ (if you are using it) and before all other middleware: .. code-block:: python MIDDLEWARE = [ # ... "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", # ... ] That's it -- WhiteNoise will now serve your static files (you can confirm it's working using the :ref:`steps below `). However, to get the best performance you should proceed to step 3 below and enable compression and caching. .. note:: You might find other third-party middleware that suggests it should be given highest priority at the top of the middleware list. Unless you understand exactly what is happening you should ignore this advice and always place ``WhiteNoiseMiddleware`` above other middleware. If you plan to have other middleware run before WhiteNoise you should be aware of the `request_finished bug `_ in Django. .. _compression-and-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``: .. code-block:: python STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" 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: .. code-block:: python STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" .. note:: If you are having problems after switching to the WhiteNoise storage backend please see the :ref:`troubleshooting guide `. .. _ManifestStaticFilesStorage: https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/#manifeststaticfilesstorage If you need to compress files outside of the static files storage system you can use the supplied :ref:`command line utility ` .. _brotli-compression: Brotli compression ++++++++++++++++++ As well as the common gzip compression format, WhiteNoise supports the newer, more efficient `brotli `_ format. This helps reduce bandwidth and increase loading speed. To enable brotli compression you will need the `Brotli Python package `_ installed by running ``pip install whitenoise[brotli]``. Brotli is supported by `all major browsers `_ (except IE11). WhiteNoise will only serve brotli data to browsers which request it so there are no compatibility issues with enabling brotli support. Also note that browsers will only request brotli data over an HTTPS connection. .. _cdn: 4. Use a Content-Delivery Network --------------------------------- The above steps will get you decent performance on moderate traffic sites, however for higher traffic sites, or sites where performance is a concern you should look at using a CDN. Because WhiteNoise sends appropriate cache headers with your static content, the CDN will be able to cache your files and serve them without needing to contact your application again. Below are instruction for setting up WhiteNoise with Amazon CloudFront, a popular choice of CDN. The process for other CDNs should look very similar though. Instructions for Amazon CloudFront ++++++++++++++++++++++++++++++++++ Go to CloudFront section of the AWS Web Console, and click "Create Distribution". Put your application's domain (without the http prefix) in the "Origin Domain Name" field and leave the rest of the settings as they are. It might take a few minutes for your distribution to become active. Once it's ready, copy the distribution domain name into your ``settings.py`` file so it looks something like this: .. code-block:: python STATIC_HOST = "https://d4663kmspf1sqa.cloudfront.net" if not DEBUG else "" STATIC_URL = STATIC_HOST + "/static/" Or, even better, you can avoid hardcoding your CDN into your settings by doing something like this: .. code-block:: python STATIC_HOST = os.environ.get("DJANGO_STATIC_HOST", "") STATIC_URL = STATIC_HOST + "/static/" This way you can configure your CDN just by setting an environment variable. For apps on Heroku, you'd run this command .. code-block:: bash heroku config:set DJANGO_STATIC_HOST=https://d4663kmspf1sqa.cloudfront.net Using compression algorithms other than gzip ++++++++++++++++++++++++++++++++++++++++++++ By default, CloudFront will discard any ``Accept-Encoding`` header browsers include in requests, unless the value of the header is gzip. If it is gzip, CloudFront will fetch the uncompressed file from the origin, compress it, and return it to the requesting browser. To get CloudFront to not do the compression itself as well as serve files compressed using other algorithms, such as Brotli, you must configure your distribution to `cache based on the Accept-Encoding header`__. You can do this in the ``Behaviours`` tab of your distribution. .. __: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html#compressed-content-custom-origin .. note:: By default your entire site will be accessible via the CloudFront URL. It's possible that this can cause SEO problems if these URLs start showing up in search results. You can restrict CloudFront to only proxy your static files by following :ref:`these directions `. .. _runserver-nostatic: 5. Using WhiteNoise in development ---------------------------------- In development Django's ``runserver`` automatically takes over static file handling. In most cases this is fine, however this means that some of the improvements that WhiteNoise makes to static file handling won't be available in development and it opens up the possibility for differences in behaviour between development and production environments. For this reason it's a good idea to use WhiteNoise in development as well. You can disable Django's static file handling and allow WhiteNoise to take over simply by passing the ``--nostatic`` option to the ``runserver`` command, but you need to remember to add this option every time you call ``runserver``. An easier way is to edit your ``settings.py`` file and add ``whitenoise.runserver_nostatic`` to the top of your ``INSTALLED_APPS`` list: .. code-block:: python INSTALLED_APPS = [ # ... "whitenoise.runserver_nostatic", "django.contrib.staticfiles", # ... ] .. note:: In older versions of WhiteNoise (below v4.0) it was not possible to use ``runserver_nostatic`` with `Channels`_ as Channels provides its own implementation of runserver. Newer versions of WhiteNoise do not have this problem and will work with Channels or any other third-party app that provides its own implementation of runserver. .. _Channels: https://channels.readthedocs.io/ .. _index-files-django: 6. Index Files -------------- When the :any:`WHITENOISE_INDEX_FILE` option is enabled: * Visiting ``/example/`` will serve the file at ``/example/index.html`` * Visiting ``/example`` will redirect (302) to ``/example/`` * Visiting ``/example/index.html`` will redirect (302) to ``/example/`` If you want to something other than ``index.html`` as the index file, then you can also set this option to an alternative filename. Available Settings ------------------ The WhiteNoiseMiddleware class takes all the same configuration options as the WhiteNoise base class, but rather than accepting keyword arguments to its constructor it uses Django settings. The setting names are just the keyword arguments upper-cased with a 'WHITENOISE\_' prefix. .. attribute:: WHITENOISE_ROOT :default: ``None`` Absolute path to a directory of files which will be served at the root of your application (ignored if not set). Don't use this for the bulk of your static files because you won't benefit from cache versioning, but it can be convenient for files like ``robots.txt`` or ``favicon.ico`` which you want to serve at a specific URL. .. attribute:: WHITENOISE_AUTOREFRESH :default: ``settings.DEBUG`` Recheck the filesystem to see if any files have changed before responding. This is designed to be used in development where it can be convenient to pick up changes to static files without restarting the server. For both performance and security reasons, this setting should not be used in production. .. attribute:: WHITENOISE_USE_FINDERS :default: ``settings.DEBUG`` Instead of only picking up files collected into ``STATIC_ROOT``, find and serve files in their original directories using Django's "finders" API. This is useful in development where it matches the behaviour of the old ``runserver`` command. It's also possible to use this setting in production, avoiding the need to run the ``collectstatic`` command during the build, so long as you do not wish to use any of the caching and compression features provided by the storage backends. .. attribute:: WHITENOISE_MAX_AGE :default: ``60 if not settings.DEBUG else 0`` 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 including a hash of their contents in the name) are detected automatically and set to be cached forever. The default is chosen to be short enough not to cause problems with stale versions but 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. .. attribute:: WHITENOISE_INDEX_FILE :default: ``False`` If ``True`` enable :ref:`index file serving `. If set to a non-empty string, enable index files and use that string as the index file name. .. attribute:: WHITENOISE_MIMETYPES :default: ``None`` A dictionary mapping file extensions (lowercase) to the mimetype for that extension. For example: :: {'.foo': 'application/x-foo'} 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 ` file. In addition to file extensions, mimetypes can be specified by supplying the entire filename, for example: :: {'some-special-file': 'application/x-custom-type'} .. attribute:: WHITENOISE_CHARSET :default: ``'utf-8'`` Charset to add as part of the ``Content-Type`` header for all files whose mimetype allows a charset. .. attribute:: WHITENOISE_ALLOW_ALL_ORIGINS :default: ``True`` Toggles whether to send an ``Access-Control-Allow-Origin: *`` header for all static files. This allows cross-origin requests for static files which means your static files will continue to work as expected even if they are served via a CDN and therefore on a different domain. Without this your static files will *mostly* work, but you may have problems with fonts loading in Firefox, or accessing images in canvas elements, or other mysterious things. The W3C `explicitly state`__ that this behaviour is safe for publicly accessible files. .. __: https://www.w3.org/TR/cors/#security .. attribute:: WHITENOISE_SKIP_COMPRESS_EXTENSIONS :default: ``('jpg', 'jpeg', 'png', 'gif', 'webp','zip', 'gz', 'tgz', 'bz2', 'tbz', 'xz', 'br', 'swf', 'flv', 'woff', 'woff2')`` File extensions to skip when compressing. Because the compression process will only create compressed files where this results in an actual size saving, it would be safe to leave this list empty and attempt to compress all files. However, for files which we're confident won't benefit from compression, it speeds up the process if we just skip over them. .. attribute:: WHITENOISE_ADD_HEADERS_FUNCTION :default: ``None`` Reference to a function which is passed the headers object for each static file, allowing it to modify them. For example: :: def force_download_pdfs(headers, path, url): if path.endswith('.pdf'): headers['Content-Disposition'] = 'attachment' WHITENOISE_ADD_HEADERS_FUNCTION = force_download_pdfs The function is passed: headers A `wsgiref.headers`__ instance (which you can treat just as a dict) containing the headers for the current file path The absolute path to the local file url The host-relative URL of the file e.g. ``/static/styles/app.css`` The function should not return anything; changes should be made by modifying the headers dictionary directly. .. __: https://docs.python.org/3/library/wsgiref.html#module-wsgiref.headers .. attribute:: WHITENOISE_IMMUTABLE_FILE_TEST :default: See :file:`immutable_file_test ` in source Reference to function, or string. If a reference to a function, this is passed the path and URL for each static file and should return whether that file is immutable, i.e. guaranteed not to change, and so can be safely cached forever. The default is designed to work with Django's ManifestStaticFilesStorage backend, and any derivatives of that, so you should only need to change this if you are using a different system for versioning your static files. If a string, this is treated as a regular expression and each file's URL is matched against it. Example: :: def immutable_file_test(path, url): # Match filename with 12 hex digits before the extension # e.g. app.db8f2edc0c8a.js return re.match(r'^.+\.[0-9a-f]{12}\..+$', url) WHITENOISE_IMMUTABLE_FILE_TEST = immutable_file_test The function is passed: path The absolute path to the local file url The host-relative URL of the file e.g. ``/static/styles/app.css`` .. attribute:: WHITENOISE_STATIC_PREFIX :default: Path component of ``settings.STATIC_URL`` (with ``settings.FORCE_SCRIPT_NAME`` removed if set) The URL prefix under which static files will be served. Usually this can be determined automatically by using the path component of ``STATIC_URL``. So if ``STATIC_URL`` is ``https://example.com/static/`` then ``WHITENOISE_STATIC_PREFIX`` will be ``/static/``. If your application is not running at the root of the domain and ``FORCE_SCRIPT_NAME`` is set then this value will be removed from the ``STATIC_URL`` path first to give the correct prefix. If your deployment is more complicated than this (for instance, if you are using a CDN which is doing path rewriting) then you may need to configure this value directly. .. attribute:: WHITENOISE_KEEP_ONLY_HASHED_FILES :default: ``False`` Stores only files with hashed names in ``STATIC_ROOT``. By default, Django's hashed static files system creates two copies of each file in ``STATIC_ROOT``: one using the original name, e.g. ``app.js``, and one using the hashed name, e.g. ``app.db8f2edc0c8a.js``. If WhiteNoise's compression backend is being used this will create another two copies of each of these files (using Gzip and Brotli compression) resulting in six output files for each input file. In some deployment scenarios it can be important to reduce the size of the build artifact as much as possible. This setting removes the "un-hashed" version of the file (which should be not be referenced in any case) which should reduce the space required for static files by half. Note, this setting is only effective if the WhiteNoise storage backend is being used. .. attribute:: WHITENOISE_MANIFEST_STRICT :default: ``True`` Set to ``False`` to prevent Django throwing an error if you reference a static file which doesn't exist in the manifest. Note, if the static file does not exist, it will still throw an error. This works by setting the manifest_strict_ option on the underlying Django storage instance, as described in the Django documentation: If a file isn't found in the ``staticfiles.json`` manifest at runtime, a ``ValueError`` is raised. This behavior can be disabled by subclassing ``ManifestStaticFilesStorage`` and setting the ``manifest_strict`` attribute to ``False`` -- nonexistent paths will remain unchanged. Note, this setting is only effective if the WhiteNoise storage backend is being used. .. _manifest_strict: https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/#django.contrib.staticfiles.storage.ManifestStaticFilesStorage.manifest_strict Additional Notes ---------------- Django Compressor +++++++++++++++++ For performance and security reasons WhiteNoise does not check for new 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 -------------------------------------------------------------------------- Serving Media Files +++++++++++++++++++ WhiteNoise is not suitable for serving user-uploaded "media" files. For one thing, as described above, it only checks for static files at startup and so files added after the app starts won't be seen. More importantly though, serving user-uploaded files from the same domain as your main application is a security risk (this `blog post`_ from Google security describes the problem well). And in addition to that, using local disk to store and serve your user media makes it harder to scale your application across multiple machines. For all these reasons, it's much better to store files on a separate dedicated storage service and serve them to users from there. The `django-storages`_ library provides many options e.g. Amazon S3, Azure Storage, and Rackspace CloudFiles. .. _blog post: https://security.googleblog.com/2012/08/content-hosting-for-modern-web.html .. _django-storages: https://django-storages.readthedocs.io/ -------------------------------------------------------------------------- .. _check-its-working: How do I know it's working? +++++++++++++++++++++++++++ You can confirm that WhiteNoise is installed and configured correctly by running you application locally with ``DEBUG`` disabled and checking that your static files still load. First you need to run ``collectstatic`` to get your files in the right place: .. code-block:: bash python manage.py collectstatic Then make sure ``DEBUG`` is set to ``False`` in your ``settings.py`` and start the server: .. code-block:: bash python manage.py runserver You should find that your static files are served, just as they would be in production. -------------------------------------------------------------------------- .. _storage-troubleshoot: Troubleshooting the WhiteNoise Storage backend ++++++++++++++++++++++++++++++++++++++++++++++ If you're having problems with the WhiteNoise storage backend, the chances are they're due to the underlying Django storage engine. This is because WhiteNoise only adds a thin wrapper around Django's storage to add compression support, and because the compression code is very simple it generally doesn't cause problems. The most common issue is that there are CSS files which reference other files (usually images or fonts) which don't exist at that specified path. When Django attempts to rewrite these references it looks for the corresponding file and throws an error if it can't find it. To test whether the problems are due to WhiteNoise or not, try swapping the WhiteNoise storage backend for the Django one: .. code-block:: python STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" If the problems persist then your issue is with Django itself (try the docs_ or the `mailing list`_). If the problem only occurs with WhiteNoise then raise a ticket on the `issue tracker`_. .. _docs: https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/ .. _mailing list: https://groups.google.com/d/forum/django-users .. _issue tracker: https://github.com/evansd/whitenoise/issues -------------------------------------------------------------------------- .. _restricting-cloudfront: Restricting CloudFront to static files ++++++++++++++++++++++++++++++++++++++ The instructions for setting up CloudFront given above will result in the entire site being accessible via the CloudFront URL. It's possible that this can cause SEO problems if these URLs start showing up in search results. You can restrict CloudFront to only proxy your static files by following these directions: 1. Go to your newly created distribution and click "*Distribution Settings*", then the "*Behaviors*" tab, then "*Create Behavior*". Put ``static/*`` into the path pattern and click "*Create*" to save. 2. Now select the ``Default (*)`` behaviour and click "*Edit*". Set "*Restrict Viewer Access*" to "*Yes*" and then click "*Yes, Edit*" to save. 3. Check that the ``static/*`` pattern is first on the list, and the default one is second. This will ensure that requests for static files are passed through but all others are blocked. Using other storage backends ++++++++++++++++++++++++++++ WhiteNoise will only work with storage backends that stores their files on the local filesystem in ``STATIC_ROOT``. It will not work with backends that store files remotely, for instance on Amazon S3. WhiteNoise makes my tests run slow! +++++++++++++++++++++++++++++++++++ WhiteNoise is designed to do as much work as possible upfront when the application starts so that it can serve files as efficiently as possible while the application is running. This makes sense for long-running production processes, but you might find that the added startup time is a problem during test runs when application instances are frequently being created and destroyed. The simplest way to fix this is to make sure that during testing the ``WHITENOISE_AUTOREFRESH`` setting is set to ``True``. (By default it is ``True`` when ``DEBUG`` is enabled and ``False`` otherwise.) This stops WhiteNoise from scanning your static files on start up but other than that its behaviour should be exactly the same. It is also worth making sure you don't have unnecessary files in your ``STATIC_ROOT`` directory. In particular, be careful not to include a ``node_modules`` directory which can contain a very large number of files and significantly slow down your application startup. If you need to include specific files from ``node_modules`` then you can create symlinks from within 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 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 .. code-block:: sh python manage.py findstatic --verbosity 2 foo which will show you all the paths which Django searches for the file "foo". If, for some reason, you want Django to silently ignore such errors you can set ``WHITENOISE_MANIFEST_STRICT`` to ``False``. Using WhiteNoise with Webpack / Browserify / $LATEST_JS_THING +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ A simple technique for integrating any frontend build system with Django is to use a directory layout like this: .. code-block:: sh ./static_src ↓ $ ./node_modules/.bin/webpack ↓ ./static_build ↓ $ ./manage.py collectstatic ↓ ./static_root Here ``static_src`` contains all the source files (JS, CSS, etc) for your project. Your build tool (which can be Webpack, Browserify or whatever you choose) then processes these files and writes the output into ``static_build``. The path to the ``static_build`` directory is added to ``settings.py``: .. code-block:: python STATICFILES_DIRS = [BASE_DIR / "static_build"] This means that Django can find the processed files, but doesn't need to know anything about the tool which produced them. The final ``manage.py collectstatic`` step writes "hash-versioned" and compressed copies of the static files into ``static_root`` ready for production. Note, both the ``static_build`` and ``static_root`` directories should be excluded from version control (e.g. through ``.gitignore``) and only the ``static_src`` directory should be checked in. Deploying an application which is not at the root of the domain +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Sometimes Django apps are deployed at a particular prefix (or "subdirectory") on a domain e.g. https://example.com/my-app/ rather than just https://example.com. In this case you would normally use Django's `FORCE_SCRIPT_NAME `_ setting to tell the application where it is located. You would also need to ensure that ``STATIC_URL`` uses the correct prefix as well. For example: .. code-block:: python FORCE_SCRIPT_NAME = "/my-app" STATIC_URL = FORCE_SCRIPT_NAME + "/static/" If you have set these two values then WhiteNoise will automatically configure itself correctly. If you are doing something more complex you may need to set :any:`WHITENOISE_STATIC_PREFIX` explicitly yourself. whitenoise-6.0.0/docs/flask.rst000066400000000000000000000057041420103656700164670ustar00rootroot00000000000000Using WhiteNoise with Flask ============================ This guide walks you through setting up a Flask project with WhiteNoise. In most cases it shouldn't take more than a couple of lines of configuration. 1. Make sure where your *static* is located ------------------------------------------- If you're familiar with Flask you'll know what to do. If you're just getting started with a new Flask project then the default is the ``static`` folder in the root path of the application. Check the ``static_folder`` argument in `Flask Application Object documentation `_ for further information. 2. Enable WhiteNoise -------------------- In the file where you create your app you instantiate Flask Application Object (the ``flask.Flask()`` object). All you have to do is to wrap it with ``WhiteNoise()`` object. If you use Flask quick start approach it will look something like that: .. code-block:: python from flask import Flask from whitenoise import WhiteNoise app = Flask(__name__) app.wsgi_app = WhiteNoise(app.wsgi_app, root="static/") If you opt for the `pattern of creating your app with a function `_, then it would look like that: .. code-block:: python from flask import Flask from sqlalchemy import create_engine from whitenoise import WhiteNoise from myapp import config from myapp.views import frontend def create_app(database_uri, debug=False): app = Flask(__name__) app.debug = debug # set up your database app.engine = create_engine(database_uri) # register your blueprints app.register_blueprint(frontend) # add whitenoise app.wsgi_app = WhiteNoise(app.wsgi_app, root="static/") # other setup tasks return app That's it -- WhiteNoise will now serve your static files. 3. Custom *static* folder ------------------------- If it turns out that you are not using the Flask default for *static* folder, fear not. You can instantiate WhiteNoise and add your *static* folders later: .. code-block:: python from flask import Flask from whitenoise import WhiteNoise app = Flask(__name__) app.wsgi_app = WhiteNoise(app.wsgi_app) my_static_folders = ( "static/folder/one/", "static/folder/two/", "static/folder/three/", ) for static in my_static_folders: app.wsgi_app.add_files(static) See the ``WhiteNoise.add_files`` documentation for further customization. 4. Prefix ------------------------- By default, WhiteNoise will serve up static files from the URL root -- i.e., ``http://localhost:5000/style.css``. To change that, set a `prefix `_ string: .. code-block:: python app.wsgi_app = WhiteNoise(app.wsgi_app, root="static/", prefix="assets/") Now, *style.css* will be available at ``http://localhost:5000/assets/style.css``. whitenoise-6.0.0/docs/index.rst000066400000000000000000000205421420103656700164730ustar00rootroot00000000000000WhiteNoise ========== .. 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 own static files, making it a self-contained unit that can be deployed anywhere without relying on nginx, Amazon S3 or any other external service. (Especially useful on Heroku, OpenShift and other PaaS providers.) It's designed to work nicely with a CDN for high-traffic sites so you don't have to sacrifice performance to benefit from simplicity. WhiteNoise works with any WSGI-compatible app but has some special auto-configuration features for Django. WhiteNoise takes care of best-practices for you, for instance: * Serving compressed content (gzip and Brotli formats, handling Accept-Encoding and Vary headers correctly) * Setting far-future cache headers on content which won't change Worried that serving static files with Python is horribly inefficient? Still think you should be using Amazon S3? Have a look at the `Infrequently Asked Questions`_ below. Installation ------------ Install with: .. code-block:: sh pip install whitenoise QuickStart for Django apps -------------------------- Edit your ``settings.py`` file and add WhiteNoise to the ``MIDDLEWARE`` list, above all other middleware apart from Django's `SecurityMiddleware `_: .. code-block:: python MIDDLEWARE = [ # ... "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", # ... ] That's it, you're ready to go. Want forever-cacheable files and compression support? Just add this to your ``settings.py``: .. code-block:: python STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" For more details, including on setting up CloudFront and other CDNs see the :doc:`Using WhiteNoise with Django ` guide. QuickStart for other WSGI apps ------------------------------ To enable WhiteNoise you need to wrap your existing WSGI application in a WhiteNoise instance and tell it where to find your static files. For example: .. code-block:: python from whitenoise import WhiteNoise from my_project import MyWSGIApp application = MyWSGIApp() application = WhiteNoise(application, root="/path/to/static/files") application.add_files("/path/to/more/static/files", prefix="more-files/") And that's it, you're ready to go. For more details see the :doc:`full documentation `. Using WhiteNoise with Flask --------------------------- WhiteNoise was not specifically written with Flask in mind, but as Flask uses the standard WSGI protocol it is easy to integrate with WhiteNoise (see the :doc:`Using WhiteNoise with Flask ` guide). Compatibility ------------- WhiteNoise works with any WSGI-compatible application and is tested on Python **3.7** – **3.10**, on both Linux and Windows. Django WhiteNoiseMiddlware is tested with Django versions **2.2** --- **4.0** Endorsements ------------ WhiteNoise owes its initial popularity to the nice things that some of Django and pip's core developers said about it: `@jezdez `_: *[WhiteNoise] is really awesome and should be the standard for Django + Heroku* `@dstufft `_: *WhiteNoise looks pretty excellent.* `@idangazit `_ *Received a positive brainsmack from @_EvansD's WhiteNoise. Vastly smarter than S3 for static assets. What was I thinking before?* It's now being used by thousands of projects, including some high-profile sites such as `mozilla.org `_. Issues & Contributing --------------------- Raise an issue on the `GitHub project `_ or feel free to nudge `@_EvansD `_ on Twitter. Infrequently Asked Questions ---------------------------- Isn't serving static files from Python horribly inefficient? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The short answer to this is that if you care about performance and efficiency then you should be using WhiteNoise behind a CDN like CloudFront. If you're doing *that* then, because of the caching headers WhiteNoise sends, the vast majority of static requests will be served directly by the CDN without touching your application, so it really doesn't make much difference how efficient WhiteNoise is. That said, WhiteNoise is pretty efficient. Because it only has to serve a fixed set of files it does all the work of finding files and determining the correct headers upfront on initialization. Requests can then be served with little more than a dictionary lookup to find the appropriate response. Also, when used with gunicorn (and most other WSGI servers) the actual business of pushing the file down the network interface is handled by the kernel's very efficient ``sendfile`` syscall, not by Python. Shouldn't I be pushing my static files to S3 using something like Django-Storages? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ No, you shouldn't. The main problem with this approach is that Amazon S3 cannot currently selectively serve compressed content to your users. Compression (using either the venerable gzip or the more modern brotli algorithms) can make dramatic reductions in the bandwidth required for your CSS and JavaScript. But in order to do this correctly the server needs to examine the ``Accept-Encoding`` header of the request to determine which compression formats are supported, and return an appropriate ``Vary`` header so that intermediate caches know to do the same. This is exactly what WhiteNoise does, but Amazon S3 currently provides no means of doing this. The second problem with a push-based approach to handling static files is that it adds complexity and fragility to your deployment process: extra libraries specific to your storage backend, extra configuration and authentication keys, and extra tasks that must be run at specific points in the deployment in order for everything to work. With the CDN-as-caching-proxy approach that WhiteNoise takes there are just two bits of configuration: your application needs the URL of the CDN, and the CDN needs the URL of your application. Everything else is just standard HTTP semantics. This makes your deployments simpler, your life easier, and you happier. What's the point in WhiteNoise when I can do the same thing in a few lines of Apache/nginx config? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ There are two answers here. One is that WhiteNoise is designed to work in situations where Apache, nginx and the like aren't easily available. But more importantly, it's easy to underestimate what's involved in serving static files correctly. Does your few lines of nginx config distinguish between files which might change and files which will never change and set the cache headers appropriately? Did you add the right CORS headers so that your fonts load correctly when served via a CDN? Did you turn on the special nginx setting which allows it to send gzipped content in response to an ``HTTP/1.0`` request, which for some reason CloudFront still uses? Did you install the extension which allows you to serve pre-compressed brotli-encoded content to modern browsers? None of this is rocket science, but it's fiddly and annoying and WhiteNoise takes care of all it for you. License ------- MIT Licensed .. toctree:: :hidden: self django base flask changelog whitenoise-6.0.0/docs/requirements.txt000066400000000000000000000000141420103656700201060ustar00rootroot00000000000000furo sphinx whitenoise-6.0.0/pyproject.toml000066400000000000000000000004721420103656700166160ustar00rootroot00000000000000[build-system] requires = ["setuptools >= 40.6.0", "wheel"] build-backend = "setuptools.build_meta" [tool.black] target-version = ['py37'] [tool.isort] profile = "black" add_imports = "from __future__ import annotations" [tool.pytest.ini_options] addopts = """\ --strict-config --strict-markers """ whitenoise-6.0.0/requirements/000077500000000000000000000000001420103656700164225ustar00rootroot00000000000000whitenoise-6.0.0/requirements/compile.py000077500000000000000000000105471420103656700204360ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import annotations import os import subprocess import sys 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) common_args = [ "-m", "piptools", "compile", # "--generate-hashes", "--allow-unsafe", ] + sys.argv[1:] subprocess.run( [ "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", "-o", "py37-django30.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.7", *common_args, "-P", "Django>=3.1a1,<3.2", "-o", "py37-django31.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.7", *common_args, "-P", "Django>=3.2a1,<3.3", "-o", "py37-django32.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.8", *common_args, "-P", "Django>=2.2,<2.3", "-o", "py38-django22.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.8", *common_args, "-P", "Django>=3.0a1,<3.1", "-o", "py38-django30.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.8", *common_args, "-P", "Django>=3.1a1,<3.2", "-o", "py38-django31.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.8", *common_args, "-P", "Django>=3.2a1,<3.3", "-o", "py38-django32.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.8", *common_args, "-P", "Django>=4.0a1,<4.1", "-o", "py38-django40.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.9", *common_args, "-P", "Django>=2.2,<2.3", "-o", "py39-django22.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.9", *common_args, "-P", "Django>=3.0a1,<3.1", "-o", "py39-django30.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.9", *common_args, "-P", "Django>=3.1a1,<3.2", "-o", "py39-django31.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.9", *common_args, "-P", "Django>=3.2a1,<3.3", "-o", "py39-django32.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.9", *common_args, "-P", "Django>=4.0a1,<4.1", "-o", "py39-django40.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.10", *common_args, "-P", "Django>=3.2a1,<3.3", "-o", "py310-django32.txt", ], check=True, capture_output=True, ) subprocess.run( [ "python3.10", *common_args, "-P", "Django>=4.0a1,<4.1", "-o", "py310-django40.txt", ], check=True, capture_output=True, ) whitenoise-6.0.0/requirements/py310-django32.txt000066400000000000000000000015651420103656700214530ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with python 3.10 # 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.2.12 # via -r requirements.in idna==3.3 # via requests 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 whitenoise-6.0.0/requirements/py310-django40.txt000066400000000000000000000015261420103656700214470ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with python 3.10 # 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==4.0.2 # via -r requirements.in idna==3.3 # via requests 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 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 whitenoise-6.0.0/requirements/py37-django22.txt000066400000000000000000000020161420103656700213700ustar00rootroot00000000000000# # 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 whitenoise-6.0.0/requirements/py37-django30.txt000066400000000000000000000021061420103656700213670ustar00rootroot00000000000000# # 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 whitenoise-6.0.0/requirements/py37-django31.txt000066400000000000000000000021061420103656700213700ustar00rootroot00000000000000# # 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 whitenoise-6.0.0/requirements/py37-django32.txt000066400000000000000000000021061420103656700213710ustar00rootroot00000000000000# # 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.2.12 # 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 whitenoise-6.0.0/requirements/py38-django22.txt000066400000000000000000000016621420103656700213770ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with python 3.8 # 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 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 whitenoise-6.0.0/requirements/py38-django30.txt000066400000000000000000000017221420103656700213730ustar00rootroot00000000000000# # 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.0.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 whitenoise-6.0.0/requirements/py38-django31.txt000066400000000000000000000017221420103656700213740ustar00rootroot00000000000000# # 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 whitenoise-6.0.0/requirements/py38-django32.txt000066400000000000000000000017221420103656700213750ustar00rootroot00000000000000# # 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.2.12 # 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 whitenoise-6.0.0/requirements/py38-django40.txt000066400000000000000000000017361420103656700214010ustar00rootroot00000000000000# # 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 backports.zoneinfo==0.2.1 # via django 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==4.0.2 # 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 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 whitenoise-6.0.0/requirements/py39-django22.txt000066400000000000000000000016621420103656700214000ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with python 3.9 # 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 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 whitenoise-6.0.0/requirements/py39-django30.txt000066400000000000000000000017221420103656700213740ustar00rootroot00000000000000# # 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.0.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 whitenoise-6.0.0/requirements/py39-django31.txt000066400000000000000000000017221420103656700213750ustar00rootroot00000000000000# # 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 whitenoise-6.0.0/requirements/py39-django32.txt000066400000000000000000000017221420103656700213760ustar00rootroot00000000000000# # 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.2.12 # 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 whitenoise-6.0.0/requirements/py39-django40.txt000066400000000000000000000016631420103656700214010ustar00rootroot00000000000000# # 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==4.0.2 # 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 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 whitenoise-6.0.0/requirements/requirements.in000066400000000000000000000000671420103656700215000ustar00rootroot00000000000000Brotli coverage django pytest pytest-randomly requests whitenoise-6.0.0/scripts/000077500000000000000000000000001420103656700153665ustar00rootroot00000000000000whitenoise-6.0.0/scripts/generate_default_media_types.py000077500000000000000000000056131420103656700236310ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import annotations import argparse import http.client import re from contextlib import closing from pathlib import Path module_dir = Path(__file__).parent.resolve() media_types_py = module_dir / "../src/whitenoise/media_types.py" def main(): parser = argparse.ArgumentParser() parser.add_argument("--check", action="store_true") args = parser.parse_args() func_str = get_default_types_function() text = media_types_py.read_text() new_text = re.sub( r"def default_types.*\}", func_str, text, flags=re.DOTALL, ) if new_text != text: if args.check: print("Would write changes") return 1 else: print(f"Writing {media_types_py}") media_types_py.write_text(new_text) return 0 EXTRA_MIMETYPES = { # nginx file uses application/javascript, but HTML specification recommends # text/javascript: ".js": "text/javascript", ".md": "text/markdown", ".mjs": "text/javascript", ".woff": "application/font-woff", ".woff2": "font/woff2", "apple-app-site-association": "application/pkc7-mime", # Adobe Products - see: # https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/xdomain.html#policy-file-host-basics "crossdomain.xml": "text/x-cross-domain-policy", } FUNCTION_TEMPLATE = '''\ def default_types(): """ We use our own set of default media types rather than the system-supplied ones. This ensures consistent media type behaviour across varied environments. The defaults are based on those shipped with nginx, with some custom additions. (Auto-generated by scripts/generate_default_media_types.py) """ return {{ {entries} }}''' def get_default_types_function(): types_map = get_types_map() lines = [ f' "{suffix}": "{media_type}",' for suffix, media_type in types_map.items() ] return FUNCTION_TEMPLATE.format(entries="\n".join(lines)) def get_types_map(): nginx_data = get_nginx_data() matches = re.findall(r"(\w+/.*?)\s+(.*?);", nginx_data) types_map = {} for match in matches: media_type = match[0] # This is the default media type anyway, no point specifying # it explicitly if media_type == "application/octet-stream": continue extensions = match[1].split() for extension in extensions: types_map[f".{extension}"] = media_type types_map.update(EXTRA_MIMETYPES) return dict(sorted(types_map.items())) def get_nginx_data(): conn = http.client.HTTPSConnection("raw.githubusercontent.com") with closing(conn): conn.request("GET", "/nginx/nginx/master/conf/mime.types") response = conn.getresponse() assert response.status == 200 return response.read().decode() if __name__ == "__main__": raise SystemExit(main()) whitenoise-6.0.0/setup.cfg000066400000000000000000000031371420103656700155240ustar00rootroot00000000000000[metadata] name = whitenoise version = 6.0.0 description = Radically simplified static file serving for WSGI applications long_description = file: README.rst long_description_content_type = text/x-rst 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 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 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.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware license_file = LICENSE [options] package_dir= =src packages = find: include_package_data = True python_requires = >=3.7 zip_safe = False [options.extras_require] brotli = Brotli [options.packages.find] where = src [flake8] max-line-length = 88 extend-ignore = E203 per-file-ignores = src/whitenoise/media_types.py:E501 [coverage:run] branch = True parallel = True source = whitenoise tests [coverage:paths] source = src .tox/*/site-packages [coverage:report] show_missing = True whitenoise-6.0.0/setup.py000066400000000000000000000001121420103656700154030ustar00rootroot00000000000000from __future__ import annotations from setuptools import setup setup() whitenoise-6.0.0/src/000077500000000000000000000000001420103656700144665ustar00rootroot00000000000000whitenoise-6.0.0/src/whitenoise/000077500000000000000000000000001420103656700166445ustar00rootroot00000000000000whitenoise-6.0.0/src/whitenoise/__init__.py000066400000000000000000000001331420103656700207520ustar00rootroot00000000000000from __future__ import annotations from .base import WhiteNoise __all__ = ["WhiteNoise"] whitenoise-6.0.0/src/whitenoise/base.py000066400000000000000000000241341420103656700201340ustar00rootroot00000000000000from __future__ import annotations import os import re import warnings from posixpath import normpath 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, ) 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, **kwargs): for attr in self.config_attrs: try: value = kwargs.pop(attr) except KeyError: pass 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.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) def __call__(self, environ, start_response): path = decode_path_info(environ.get("PATH_INFO", "")) if self.autorefresh: static_file = self.find_file(path) else: static_file = self.files.get(path) if static_file is None: return self.application(environ, start_response) else: return self.serve(static_file, environ, start_response) @staticmethod def serve(static_file, environ, start_response): response = static_file.get_response(environ["REQUEST_METHOD"], environ) status_line = f"{response.status} {response.status.phrase}" start_response(status_line, list(response.headers)) if response.file is not None: file_wrapper = environ.get("wsgi.file_wrapper", FileWrapper) return file_wrapper(response.file) else: 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 # to store the list of directories in reverse order so later ones # match first when they're checked in "autorefresh" mode self.directories.insert(0, (root, prefix)) else: if os.path.isdir(root): self.update_files_dictionary(root, prefix) else: warnings.warn(f"No directory at: {root}") def update_files_dictionary(self, root, prefix): # Build a mapping from paths to the results of `os.stat` calls # so we only have to touch the filesystem once stat_cache = dict(scantree(root)) for path in stat_cache.keys(): relative_path = path[len(root) :] relative_url = relative_path.replace("\\", "/") url = prefix + relative_url self.add_file_to_dictionary(url, path, stat_cache=stat_cache) 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): index_url = url[: -len(self.index_file)] index_no_slash = index_url.rstrip("/") self.files[url] = self.redirect(url, index_url) self.files[index_no_slash] = self.redirect(index_no_slash, index_url) url = index_url static_file = self.get_static_file(path, url, stat_cache=stat_cache) self.files[url] = static_file def find_file(self, url): # Optimization: bail early if the URL can never match a file if not self.index_file and url.endswith("/"): return if not self.url_is_canonical(url): return for path in self.candidate_paths_for_url(url): try: return self.find_file_at_path(path, url) except MissingFileError: pass def candidate_paths_for_url(self, url): for root, prefix in self.directories: if url.startswith(prefix): path = os.path.join(root, url[len(prefix) :]) if os.path.commonprefix((root, path)) == root: yield path 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: 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) @staticmethod def url_is_canonical(url): """ Check that the URL path is in canonical format i.e. has normalised slashes and no path traversal elements """ if "\\" in url: return False normalised = normpath(url) if url.endswith("/") and url != "/": normalised += "/" return normalised == url @staticmethod def is_compressed_variant(path, stat_cache=None): if path[-3:] in (".gz", ".br"): uncompressed_path = path[:-3] if stat_cache is None: return os.path.isfile(uncompressed_path) else: return uncompressed_path in stat_cache return False def get_static_file(self, path, url, stat_cache=None): # Optimization: bail early if file does not exist if stat_cache is None and not os.path.exists(path): raise MissingFileError(path) headers = Headers([]) self.add_mime_headers(headers, path, url) self.add_cache_headers(headers, path, url) if self.allow_all_origins: headers["Access-Control-Allow-Origin"] = "*" if self.add_headers_function: self.add_headers_function(headers, path, url) return StaticFile( path, headers.items(), stat_cache=stat_cache, encodings={"gzip": path + ".gz", "br": path + ".br"}, ) def add_mime_headers(self, headers, path, url): media_type = self.media_types.get_type(path) if media_type.startswith("text/"): params = {"charset": str(self.charset)} else: params = {} headers.add_header("Content-Type", str(media_type), **params) def add_cache_headers(self, headers, path, url): if self.immutable_file_test(path, url): headers["Cache-Control"] = "max-age={}, public, immutable".format( self.FOREVER ) elif self.max_age is not None: headers["Cache-Control"] = f"max-age={self.max_age}, public" def immutable_file_test(self, path, url): """ This should be implemented by sub-classes (see e.g. WhiteNoiseMiddleware) or by setting the `immutable_file_test` config option """ return False def redirect(self, from_url, to_url): """ Return a relative 302 redirect We use relative redirects as we don't know the absolute URL the app is being hosted under """ if to_url == from_url + "/": relative_url = from_url.split("/")[-1] + "/" elif from_url == to_url + self.index_file: relative_url = "./" else: raise ValueError(f"Cannot handle redirect: {from_url} > {to_url}") if self.max_age is not None: headers = {"Cache-Control": f"max-age={self.max_age}, public"} else: headers = {} return Redirect(relative_url, headers=headers) def scantree(root): """ Recurse the given directory yielding (pathname, os.stat(pathname)) pairs """ for entry in os.scandir(root): if entry.is_dir(): yield from scantree(entry.path) else: yield entry.path, entry.stat() whitenoise-6.0.0/src/whitenoise/compress.py000066400000000000000000000122521420103656700210530ustar00rootroot00000000000000from __future__ import annotations import argparse import gzip import os import re from io import BytesIO try: import brotli brotli_installed = True except ImportError: # pragma: no cover brotli_installed = False class Compressor: # Extensions that it's not worth trying to compress SKIP_COMPRESS_EXTENSIONS = ( # Images "jpg", "jpeg", "png", "gif", "webp", # Compressed files "zip", "gz", "tgz", "bz2", "tbz", "xz", "br", # Flash "swf", "flv", # Fonts "woff", "woff2", ) def __init__( self, extensions=None, use_gzip=True, use_brotli=True, log=print, quiet=False ): if extensions is None: extensions = self.SKIP_COMPRESS_EXTENSIONS self.extension_re = self.get_extension_re(extensions) self.use_gzip = use_gzip self.use_brotli = use_brotli and brotli_installed if not quiet: self.log = log @staticmethod def get_extension_re(extensions): if not extensions: return re.compile("^$") else: return re.compile( r"\.({})$".format("|".join(map(re.escape, extensions))), re.IGNORECASE ) def should_compress(self, filename): return not self.extension_re.search(filename) def log(self, message): pass def compress(self, path): with open(path, "rb") as f: stat_result = os.fstat(f.fileno()) data = f.read() size = len(data) if self.use_brotli: compressed = self.compress_brotli(data) if self.is_compressed_effectively("Brotli", path, size, compressed): yield self.write_data(path, compressed, ".br", stat_result) else: # If Brotli compression wasn't effective gzip won't be either return if self.use_gzip: compressed = self.compress_gzip(data) if self.is_compressed_effectively("Gzip", path, size, compressed): yield self.write_data(path, compressed, ".gz", stat_result) @staticmethod def compress_gzip(data): output = BytesIO() # Explicitly set mtime to 0 so gzip content is fully determined # by file content (0 = "no timestamp" according to gzip spec) with gzip.GzipFile( filename="", mode="wb", fileobj=output, compresslevel=9, mtime=0 ) as gz_file: gz_file.write(data) return output.getvalue() @staticmethod def compress_brotli(data): return brotli.compress(data) def is_compressed_effectively(self, encoding_name, path, orig_size, data): compressed_size = len(data) if orig_size == 0: is_effective = False else: ratio = compressed_size / orig_size is_effective = ratio <= 0.95 if is_effective: self.log( "{} compressed {} ({}K -> {}K)".format( encoding_name, path, orig_size // 1024, compressed_size // 1024 ) ) else: self.log(f"Skipping {path} ({encoding_name} compression not effective)") return is_effective def write_data(self, path, data, suffix, stat_result): filename = path + suffix with open(filename, "wb") as f: f.write(data) os.utime(filename, (stat_result.st_atime, stat_result.st_mtime)) return filename def main(argv=None): parser = argparse.ArgumentParser( description="Search for all files inside *not* matching " " and produce compressed versions with " "'.gz' and '.br' suffixes (as long as this results in a " "smaller file)" ) parser.add_argument( "-q", "--quiet", help="Don't produce log output", action="store_true" ) parser.add_argument( "--no-gzip", help="Don't produce gzip '.gz' files", action="store_false", dest="use_gzip", ) parser.add_argument( "--no-brotli", help="Don't produce brotli '.br' files", action="store_false", dest="use_brotli", ) parser.add_argument("root", help="Path root from which to search for files") default_exclude = ", ".join(Compressor.SKIP_COMPRESS_EXTENSIONS) parser.add_argument( "extensions", nargs="*", help=( "File extensions to exclude from compression " + f"(default: {default_exclude})" ), default=Compressor.SKIP_COMPRESS_EXTENSIONS, ) args = parser.parse_args(argv) compressor = Compressor( extensions=args.extensions, use_gzip=args.use_gzip, use_brotli=args.use_brotli, quiet=args.quiet, ) for dirpath, _dirs, files in os.walk(args.root): for filename in files: if compressor.should_compress(filename): path = os.path.join(dirpath, filename) for _compressed in compressor.compress(path): pass return 0 if __name__ == "__main__": # pragma: no cover raise SystemExit(main()) whitenoise-6.0.0/src/whitenoise/media_types.py000066400000000000000000000120141420103656700215170ustar00rootroot00000000000000from __future__ import annotations import os class MediaTypes: __slots__ = ("types_map",) def __init__(self, *, extra_types=None): self.types_map = default_types() if extra_types is not None: self.types_map.update(extra_types) def get_type(self, path): name = os.path.basename(path).lower() media_type = self.types_map.get(name) if media_type is not None: return media_type extension = os.path.splitext(name)[1] return self.types_map.get(extension, "application/octet-stream") def default_types(): """ We use our own set of default media types rather than the system-supplied ones. This ensures consistent media type behaviour across varied environments. The defaults are based on those shipped with nginx, with some custom additions. (Auto-generated by scripts/generate_default_media_types.py) """ return { ".3gp": "video/3gpp", ".3gpp": "video/3gpp", ".7z": "application/x-7z-compressed", ".ai": "application/postscript", ".asf": "video/x-ms-asf", ".asx": "video/x-ms-asf", ".atom": "application/atom+xml", ".avi": "video/x-msvideo", ".avif": "image/avif", ".bmp": "image/x-ms-bmp", ".cco": "application/x-cocoa", ".crt": "application/x-x509-ca-cert", ".css": "text/css", ".der": "application/x-x509-ca-cert", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".ear": "application/java-archive", ".eot": "application/vnd.ms-fontobject", ".eps": "application/postscript", ".flv": "video/x-flv", ".gif": "image/gif", ".hqx": "application/mac-binhex40", ".htc": "text/x-component", ".htm": "text/html", ".html": "text/html", ".ico": "image/x-icon", ".jad": "text/vnd.sun.j2me.app-descriptor", ".jar": "application/java-archive", ".jardiff": "application/x-java-archive-diff", ".jng": "image/x-jng", ".jnlp": "application/x-java-jnlp-file", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".js": "text/javascript", ".json": "application/json", ".kar": "audio/midi", ".kml": "application/vnd.google-earth.kml+xml", ".kmz": "application/vnd.google-earth.kmz", ".m3u8": "application/vnd.apple.mpegurl", ".m4a": "audio/x-m4a", ".m4v": "video/x-m4v", ".md": "text/markdown", ".mid": "audio/midi", ".midi": "audio/midi", ".mjs": "text/javascript", ".mml": "text/mathml", ".mng": "video/x-mng", ".mov": "video/quicktime", ".mp3": "audio/mpeg", ".mp4": "video/mp4", ".mpeg": "video/mpeg", ".mpg": "video/mpeg", ".odg": "application/vnd.oasis.opendocument.graphics", ".odp": "application/vnd.oasis.opendocument.presentation", ".ods": "application/vnd.oasis.opendocument.spreadsheet", ".odt": "application/vnd.oasis.opendocument.text", ".ogg": "audio/ogg", ".pdb": "application/x-pilot", ".pdf": "application/pdf", ".pem": "application/x-x509-ca-cert", ".pl": "application/x-perl", ".pm": "application/x-perl", ".png": "image/png", ".ppt": "application/vnd.ms-powerpoint", ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".prc": "application/x-pilot", ".ps": "application/postscript", ".ra": "audio/x-realaudio", ".rar": "application/x-rar-compressed", ".rpm": "application/x-redhat-package-manager", ".rss": "application/rss+xml", ".rtf": "application/rtf", ".run": "application/x-makeself", ".sea": "application/x-sea", ".shtml": "text/html", ".sit": "application/x-stuffit", ".svg": "image/svg+xml", ".svgz": "image/svg+xml", ".swf": "application/x-shockwave-flash", ".tcl": "application/x-tcl", ".tif": "image/tiff", ".tiff": "image/tiff", ".tk": "application/x-tcl", ".ts": "video/mp2t", ".txt": "text/plain", ".war": "application/java-archive", ".wasm": "application/wasm", ".wbmp": "image/vnd.wap.wbmp", ".webm": "video/webm", ".webp": "image/webp", ".wml": "text/vnd.wap.wml", ".wmlc": "application/vnd.wap.wmlc", ".wmv": "video/x-ms-wmv", ".woff": "application/font-woff", ".woff2": "font/woff2", ".xhtml": "application/xhtml+xml", ".xls": "application/vnd.ms-excel", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xml": "text/xml", ".xpi": "application/x-xpinstall", ".xspf": "application/xspf+xml", ".zip": "application/zip", "apple-app-site-association": "application/pkc7-mime", "crossdomain.xml": "text/x-cross-domain-policy", } whitenoise-6.0.0/src/whitenoise/middleware.py000066400000000000000000000144411420103656700213370ustar00rootroot00000000000000from __future__ import annotations import os from posixpath import basename from urllib.parse import urlparse from django.conf import settings from django.contrib.staticfiles import finders from django.contrib.staticfiles.storage import staticfiles_storage 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 __all__ = ["WhiteNoiseMiddleware"] class WhiteNoiseFileResponse(FileResponse): """ Wrap Django's FileResponse to prevent setting any default headers. For the most part these just duplicate work already done by WhiteNoise but in some cases (e.g. the content-disposition header introduced in Django 3.0) they are actively harmful. """ def set_headers(self, *args, **kwargs): pass 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. """ 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) if self.static_root: self.add_files(self.static_root, prefix=self.static_prefix) if self.root: self.add_files(self.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) @staticmethod def serve(static_file, request): response = static_file.get_response(request.method, request.META) status = int(response.status) http_response = WhiteNoiseFileResponse(response.file or (), status=status) # Remove default content-type del http_response["content-type"] for key, value in response.headers: 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(): for path, storage in finder.list(None): prefix = (getattr(storage, "prefix", None) or "").strip("/") url = "".join( ( self.static_prefix, prefix, "/" if prefix else "", path.replace("\\", "/"), ) ) # Use setdefault as only first matching file should be used files.setdefault(url, storage.path(path)) stat_cache = {path: os.stat(path) for path in files.values()} for url, path in files.items(): self.add_file_to_dictionary(url, path, stat_cache=stat_cache) def candidate_paths_for_url(self, url): if self.use_finders and url.startswith(self.static_prefix): path = finders.find(url[len(self.static_prefix) :]) if path: yield path paths = super().candidate_paths_for_url(url) for path in paths: yield path def immutable_file_test(self, path, url): """ Determine whether given URL represents an immutable file (i.e. a file with a hash of its contents as part of its name) which can therefore be cached forever """ if not url.startswith(self.static_prefix): return False name = url[len(self.static_prefix) :] name_without_hash = self.get_name_without_hash(name) if name == name_without_hash: return False static_url = self.get_static_url(name_without_hash) # If the static_url function maps the name without hash # back to the original name, then we know we've got a # versioned filename if static_url and basename(static_url) == basename(url): return True return False def get_name_without_hash(self, filename): """ Removes the version hash from a filename e.g, transforms 'css/application.f3ea4bcc2.css' into 'css/application.css' Note: this is specific to the naming scheme used by Django's CachedStaticFilesStorage. You may have to override this if you are using a different static files versioning system """ name_with_hash, ext = os.path.splitext(filename) name = os.path.splitext(name_with_hash)[0] return name + ext def get_static_url(self, name): try: return decode_if_byte_string(staticfiles_storage.url(name)) except ValueError: return None whitenoise-6.0.0/src/whitenoise/responders.py000066400000000000000000000232131420103656700214030ustar00rootroot00000000000000from __future__ import annotations import errno import os import re import stat from email.utils import formatdate, parsedate from http import HTTPStatus from io import BufferedIOBase from time import mktime from urllib.parse import quote from wsgiref.headers import Headers class Response: __slots__ = ("status", "headers", "file") def __init__(self, status, headers, file): self.status = status self.headers = headers self.file = file NOT_ALLOWED_RESPONSE = Response( status=HTTPStatus.METHOD_NOT_ALLOWED, headers=[("Allow", "GET, HEAD")], file=None, ) # Headers which should be returned with a 304 Not Modified response as # specified here: https://tools.ietf.org/html/rfc7232#section-4.1 NOT_MODIFIED_HEADERS = ( "Cache-Control", "Content-Location", "Date", "ETag", "Expires", "Vary", ) class SlicedFile(BufferedIOBase): """ A file like wrapper to handle seeking to the start byte of a range request and to return no further output once the end byte of a range request has been reached. """ def __init__(self, fileobj, start, end): fileobj.seek(start) self.fileobj = fileobj self.remaining = end - start + 1 def read(self, size=-1): if self.remaining <= 0: return b"" if size < 0: size = self.remaining else: size = min(size, self.remaining) data = self.fileobj.read(size) self.remaining -= len(data) return data def close(self): self.fileobj.close() class StaticFile: def __init__(self, path, headers, encodings=None, stat_cache=None): files = self.get_file_stats(path, encodings, stat_cache) headers = self.get_headers(headers, files) self.last_modified = parsedate(headers["Last-Modified"]) self.etag = headers["ETag"] self.not_modified_response = self.get_not_modified_response(headers) self.alternatives = self.get_alternatives(headers, files) def get_response(self, method, request_headers): if method not in ("GET", "HEAD"): return NOT_ALLOWED_RESPONSE if self.is_not_modified(request_headers): return self.not_modified_response path, headers = self.get_path_and_headers(request_headers) if method != "HEAD": file_handle = open(path, "rb") else: file_handle = None range_header = request_headers.get("HTTP_RANGE") if range_header: try: return self.get_range_response(range_header, headers, file_handle) except ValueError: # If we can't interpret the Range request for any reason then # just ignore it and return the standard response (this # behaviour is allowed by the spec) pass return Response(HTTPStatus.OK, headers, file_handle) def get_range_response(self, range_header, base_headers, file_handle): headers = [] for item in base_headers: if item[0] == "Content-Length": size = int(item[1]) else: headers.append(item) start, end = self.get_byte_range(range_header, size) if start >= end: return self.get_range_not_satisfiable_response(file_handle, size) if file_handle is not None: file_handle = SlicedFile(file_handle, start, end) headers.append(("Content-Range", f"bytes {start}-{end}/{size}")) headers.append(("Content-Length", str(end - start + 1))) return Response(HTTPStatus.PARTIAL_CONTENT, headers, file_handle) def get_byte_range(self, range_header, size): start, end = self.parse_byte_range(range_header) if start < 0: start = max(start + size, 0) if end is None: end = size - 1 else: end = min(end, size - 1) return start, end @staticmethod def parse_byte_range(range_header): units, _, range_spec = range_header.strip().partition("=") if units != "bytes": raise ValueError() # Only handle a single range spec. Multiple ranges will trigger a # ValueError below which will result in the Range header being ignored start_str, sep, end_str = range_spec.strip().partition("-") if not sep: raise ValueError() if not start_str: start = -int(end_str) end = None else: start = int(start_str) end = int(end_str) if end_str else None return start, end @staticmethod def get_range_not_satisfiable_response(file_handle, size): if file_handle is not None: file_handle.close() return Response( HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE, [("Content-Range", f"bytes */{size}")], None, ) @staticmethod def get_file_stats(path, encodings, stat_cache): # Primary file has an encoding of None files = {None: FileEntry(path, stat_cache)} if encodings: for encoding, alt_path in encodings.items(): try: files[encoding] = FileEntry(alt_path, stat_cache) except MissingFileError: continue return files def get_headers(self, headers_list, files): headers = Headers(headers_list) main_file = files[None] if len(files) > 1: headers["Vary"] = "Accept-Encoding" if "Last-Modified" not in headers: mtime = main_file.mtime # Not all filesystems report mtimes, and sometimes they report an # mtime of 0 which we know is incorrect if mtime: headers["Last-Modified"] = formatdate(mtime, usegmt=True) if "ETag" not in headers: last_modified = parsedate(headers["Last-Modified"]) if last_modified: timestamp = int(mktime(last_modified)) headers["ETag"] = f'"{timestamp:x}-{main_file.size:x}"' return headers @staticmethod def get_not_modified_response(headers): not_modified_headers = [] for key in NOT_MODIFIED_HEADERS: if key in headers: not_modified_headers.append((key, headers[key])) return Response( status=HTTPStatus.NOT_MODIFIED, headers=not_modified_headers, file=None ) @staticmethod def get_alternatives(base_headers, files): # Sort by size so that the smallest compressed alternative matches first alternatives = [] files_by_size = sorted(files.items(), key=lambda i: i[1].size) for encoding, file_entry in files_by_size: headers = Headers(base_headers.items()) headers["Content-Length"] = str(file_entry.size) if encoding: headers["Content-Encoding"] = encoding encoding_re = re.compile(r"\b%s\b" % encoding) else: encoding_re = re.compile("") alternatives.append((encoding_re, file_entry.path, headers.items())) return alternatives def is_not_modified(self, request_headers): previous_etag = request_headers.get("HTTP_IF_NONE_MATCH") if previous_etag is not None: return previous_etag == self.etag if self.last_modified is None: return False try: last_requested = request_headers["HTTP_IF_MODIFIED_SINCE"] except KeyError: return False last_requested_ts = parsedate(last_requested) if last_requested_ts is not None: return parsedate(last_requested) >= self.last_modified return False def get_path_and_headers(self, request_headers): accept_encoding = request_headers.get("HTTP_ACCEPT_ENCODING", "") if accept_encoding == "*": accept_encoding = "" # These are sorted by size so first match is the best for encoding_re, path, headers in self.alternatives: if encoding_re.search(accept_encoding): return path, headers class Redirect: def __init__(self, location, headers=None): headers = list(headers.items()) if headers else [] headers.append(("Location", quote(location.encode("utf8")))) self.response = Response(HTTPStatus.FOUND, headers, None) def get_response(self, method, request_headers): return self.response class NotARegularFileError(Exception): pass class MissingFileError(NotARegularFileError): pass class IsDirectoryError(MissingFileError): pass class FileEntry: __slots__ = ("path", "size", "mtime") def __init__(self, path, stat_cache=None): self.path = path stat_function = os.stat if stat_cache is None else stat_cache.__getitem__ stat = self.stat_regular_file(path, stat_function) self.size = stat.st_size self.mtime = stat.st_mtime @staticmethod def stat_regular_file(path, stat_function): """ Wrap `stat_function` to raise appropriate errors if `path` is not a regular file """ try: stat_result = stat_function(path) except KeyError: raise MissingFileError(path) except OSError as e: if e.errno in (errno.ENOENT, errno.ENAMETOOLONG): raise MissingFileError(path) else: raise if not stat.S_ISREG(stat_result.st_mode): if stat.S_ISDIR(stat_result.st_mode): raise IsDirectoryError(f"Path is a directory: {path}") else: raise NotARegularFileError(f"Not a regular file: {path}") return stat_result whitenoise-6.0.0/src/whitenoise/runserver_nostatic/000077500000000000000000000000001420103656700226035ustar00rootroot00000000000000whitenoise-6.0.0/src/whitenoise/runserver_nostatic/__init__.py000066400000000000000000000000001420103656700247020ustar00rootroot00000000000000whitenoise-6.0.0/src/whitenoise/runserver_nostatic/management/000077500000000000000000000000001420103656700247175ustar00rootroot00000000000000whitenoise-6.0.0/src/whitenoise/runserver_nostatic/management/__init__.py000066400000000000000000000000001420103656700270160ustar00rootroot00000000000000whitenoise-6.0.0/src/whitenoise/runserver_nostatic/management/commands/000077500000000000000000000000001420103656700265205ustar00rootroot00000000000000whitenoise-6.0.0/src/whitenoise/runserver_nostatic/management/commands/__init__.py000066400000000000000000000000001420103656700306170ustar00rootroot00000000000000whitenoise-6.0.0/src/whitenoise/runserver_nostatic/management/commands/runserver.py000066400000000000000000000032131420103656700311240ustar00rootroot00000000000000""" Subclass the existing 'runserver' command and change the default options to disable static file serving, allowing WhiteNoise to handle static files. There is some unpleasant hackery here because we don't know which command class to subclass until runtime as it depends on which INSTALLED_APPS we have, so we have to determine this dynamically. """ from __future__ import annotations from importlib import import_module from django.apps import apps def get_next_runserver_command(): """ Return the next highest priority "runserver" command class """ for app_name in get_lower_priority_apps(): module_path = "%s.management.commands.runserver" % app_name try: return import_module(module_path).Command except (ImportError, AttributeError): pass def get_lower_priority_apps(): """ Yield all app module names below the current app in the INSTALLED_APPS list """ self_app_name = ".".join(__name__.split(".")[:-3]) reached_self = False for app_config in apps.get_app_configs(): if app_config.name == self_app_name: reached_self = True elif reached_self: yield app_config.name yield "django.core" RunserverCommand = get_next_runserver_command() class Command(RunserverCommand): def add_arguments(self, parser): super().add_arguments(parser) if parser.get_default("use_static_handler") is True: parser.set_defaults(use_static_handler=False) parser.description += ( "\n(Wrapped by 'whitenoise.runserver_nostatic' to always" " enable '--nostatic')" ) whitenoise-6.0.0/src/whitenoise/storage.py000066400000000000000000000162631420103656700206720ustar00rootroot00000000000000from __future__ import annotations import errno import os import re import textwrap from django.conf import settings from django.contrib.staticfiles.storage import ( ManifestStaticFilesStorage, StaticFilesStorage, ) from .compress import Compressor class CompressedStaticFilesMixin: """ Wraps a StaticFilesStorage instance to compress 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 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 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 class MissingFileError(ValueError): pass class CompressedManifestStaticFilesStorage( HelpfulExceptionMixin, ManifestStaticFilesStorage ): """ Extends ManifestStaticFilesStorage instance to create compressed versions of its output files and, optionally, to delete the non-hashed files (i.e. those without the hash in their name) """ _new_files = None def __init__(self, *args, **kwargs): manifest_strict = getattr(settings, "WHITENOISE_MANIFEST_STRICT", None) if manifest_strict is not None: self.manifest_strict = manifest_strict super().__init__(*args, **kwargs) 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 def post_process_with_compression(self, files): # Files may get hashed multiple times, we want to keep track of all the # intermediate files generated during the process and which of these # are the final names used for each file. As not every intermediate # file is yielded we have to hook in to the `hashed_name` method to # keep track of them all. hashed_names = {} new_files = set() self.start_tracking_new_files(new_files) for name, hashed_name, processed in files: if hashed_name and not isinstance(processed, Exception): hashed_names[self.clean_name(name)] = hashed_name yield name, hashed_name, processed self.stop_tracking_new_files() original_files = set(hashed_names.keys()) hashed_files = set(hashed_names.values()) if self.keep_only_hashed_files: files_to_delete = (original_files | new_files) - hashed_files files_to_compress = hashed_files else: files_to_delete = set() files_to_compress = original_files | hashed_files self.delete_files(files_to_delete) for name, compressed_name in self.compress_files(files_to_compress): yield name, compressed_name, True def hashed_name(self, *args, **kwargs): name = super().hashed_name(*args, **kwargs) if self._new_files is not None: self._new_files.add(self.clean_name(name)) return name def start_tracking_new_files(self, new_files): self._new_files = new_files def stop_tracking_new_files(self): self._new_files = None @property def keep_only_hashed_files(self): return getattr(settings, "WHITENOISE_KEEP_ONLY_HASHED_FILES", False) def delete_files(self, files_to_delete): for name in files_to_delete: try: os.unlink(self.path(name)) except OSError as e: if e.errno != errno.ENOENT: raise def create_compressor(self, **kwargs): return Compressor(**kwargs) def compress_files(self, names): extensions = getattr(settings, "WHITENOISE_SKIP_COMPRESS_EXTENSIONS", None) compressor = self.create_compressor(extensions=extensions, quiet=True) for name in 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 whitenoise-6.0.0/src/whitenoise/string_utils.py000066400000000000000000000012361420103656700217460ustar00rootroot00000000000000from __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. def decode_path_info(path_info): return path_info.encode("iso-8859-1", "replace").decode("utf-8", "replace") def ensure_leading_trailing_slash(path): path = (path or "").strip("/") return f"/{path}/" if path else "/" whitenoise-6.0.0/tests/000077500000000000000000000000001420103656700150415ustar00rootroot00000000000000whitenoise-6.0.0/tests/__init__.py000066400000000000000000000000001420103656700171400ustar00rootroot00000000000000whitenoise-6.0.0/tests/conftest.py000066400000000000000000000003601420103656700172370ustar00rootroot00000000000000from __future__ import annotations import os import django import pytest @pytest.fixture(autouse=True, scope="session") def django_setup(): os.environ["DJANGO_SETTINGS_MODULE"] = "tests.django_settings" django.setup() yield whitenoise-6.0.0/tests/django_settings.py000066400000000000000000000017201420103656700205750ustar00rootroot00000000000000from __future__ import annotations import os.path from .utils import TEST_FILE_PATH, AppServer ALLOWED_HOSTS = ["*"] ROOT_URLCONF = "tests.django_urls" SECRET_KEY = "test_secret" INSTALLED_APPS = ["whitenoise.runserver_nostatic", "django.contrib.staticfiles"] FORCE_SCRIPT_NAME = "/" + AppServer.PREFIX STATIC_URL = FORCE_SCRIPT_NAME + "/static/" STATIC_ROOT = os.path.join(TEST_FILE_PATH, "root") STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" MIDDLEWARE = ["whitenoise.middleware.WhiteNoiseMiddleware"] LOGGING = { "version": 1, "disable_existing_loggers": False, "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, "handlers": {"log_to_stderr": {"level": "ERROR", "class": "logging.StreamHandler"}}, "loggers": { "django.request": { "handlers": ["log_to_stderr"], "level": "ERROR", "propagate": True, } }, } USE_TZ = True whitenoise-6.0.0/tests/django_urls.py000066400000000000000000000000651420103656700177230ustar00rootroot00000000000000from __future__ import annotations urlpatterns = [] whitenoise-6.0.0/tests/test_compress.py000066400000000000000000000046111420103656700203070ustar00rootroot00000000000000from __future__ import annotations import contextlib import gzip import os import re import shutil import tempfile import pytest from whitenoise.compress import Compressor from whitenoise.compress import main as compress_main COMPRESSABLE_FILE = "application.css" TOO_SMALL_FILE = "too-small.css" WRONG_EXTENSION = "image.jpg" TEST_FILES = {COMPRESSABLE_FILE: b"a" * 1000, TOO_SMALL_FILE: b"hi"} @pytest.fixture(scope="module", autouse=True) def files_dir(): # Make a temporary directory and copy in test files tmp = tempfile.mkdtemp() for path, contents in TEST_FILES.items(): path = os.path.join(tmp, path.lstrip("/")) try: os.makedirs(os.path.dirname(path)) except FileExistsError: pass with open(path, "wb") as f: f.write(contents) timestamp = 1498579535 os.utime(path, (timestamp, timestamp)) compress_main([tmp, "--quiet"]) yield tmp shutil.rmtree(tmp) def test_compresses_file(files_dir): with contextlib.closing( gzip.open(os.path.join(files_dir, COMPRESSABLE_FILE + ".gz"), "rb") ) as f: contents = f.read() assert TEST_FILES[COMPRESSABLE_FILE] == contents def test_doesnt_compress_if_no_saving(files_dir): assert not os.path.exists(os.path.join(files_dir, TOO_SMALL_FILE + "gz")) def test_ignores_other_extensions(files_dir): assert not os.path.exists(os.path.join(files_dir, WRONG_EXTENSION + ".gz")) def test_mtime_is_preserved(files_dir): path = os.path.join(files_dir, COMPRESSABLE_FILE) gzip_path = path + ".gz" assert os.path.getmtime(path) == os.path.getmtime(gzip_path) def test_with_custom_extensions(): compressor = Compressor(extensions=["jpg"], quiet=True) assert compressor.extension_re == re.compile(r"\.(jpg)$", re.IGNORECASE) def test_with_falsey_extensions(): compressor = Compressor(quiet=True) assert compressor.get_extension_re("") == re.compile("^$") def test_custom_log(): compressor = Compressor(log="test") assert compressor.log == "test" def test_compress(): compressor = Compressor(use_brotli=False, use_gzip=False) assert [] == list(compressor.compress("tests/test_files/static/styles.css")) def test_compressed_effectively_no_orig_size(): compressor = Compressor(quiet=True) assert not compressor.is_compressed_effectively( "test_encoding", "test_path", 0, "test_data" ) whitenoise-6.0.0/tests/test_django_whitenoise.py000066400000000000000000000160561420103656700221620ustar00rootroot00000000000000from __future__ import annotations import shutil import tempfile from contextlib import closing from urllib.parse import urljoin, urlparse import django import pytest from django.conf import settings from django.contrib.staticfiles import finders, 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 def reset_lazy_object(obj): obj._wrapped = empty def get_url_path(base, url): return urlparse(urljoin(base, url)).path @pytest.fixture() def static_files(): files = Files("static", js="app.js", nonascii="nonascii\u2713.txt") with override_settings(STATICFILES_DIRS=[files.directory]): yield files @pytest.fixture() def root_files(): files = Files("root", robots="robots.txt") with override_settings(WHITENOISE_ROOT=files.directory): yield files @pytest.fixture() def tmp(): tmp_dir = tempfile.mkdtemp() with override_settings(STATIC_ROOT=tmp_dir): yield tmp_dir shutil.rmtree(tmp_dir) @pytest.fixture() def _collect_static(static_files, root_files, tmp): reset_lazy_object(storage.staticfiles_storage) call_command("collectstatic", verbosity=0, interactive=False) @pytest.fixture() def application(_collect_static): return get_wsgi_application() @pytest.fixture() def server(application): app_server = AppServer(application) with closing(app_server): yield app_server def test_get_root_file(server, root_files, _collect_static): response = server.get(root_files.robots_url) assert response.content == root_files.robots_content def test_versioned_file_cached_forever(server, static_files, _collect_static): url = storage.staticfiles_storage.url(static_files.js_path) response = server.get(url) assert response.content == static_files.js_content assert ( response.headers.get("Cache-Control") == f"max-age={WhiteNoiseMiddleware.FOREVER}, public, immutable" ) def test_unversioned_file_not_cached_forever(server, static_files, _collect_static): 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 ) def test_get_gzip(server, static_files, _collect_static): url = storage.staticfiles_storage.url(static_files.js_path) response = server.get(url, headers={"Accept-Encoding": "gzip"}) assert response.content == static_files.js_content assert response.headers["Content-Encoding"] == "gzip" assert response.headers["Vary"] == "Accept-Encoding" def test_get_brotli(server, static_files, _collect_static): url = storage.staticfiles_storage.url(static_files.js_path) response = server.get(url, headers={"Accept-Encoding": "gzip, br"}) assert response.content == static_files.js_content assert response.headers["Content-Encoding"] == "br" assert response.headers["Vary"] == "Accept-Encoding" def test_no_content_type_when_not_modified(server, static_files, _collect_static): last_mod = "Fri, 11 Apr 2100 11:47:06 GMT" url = settings.STATIC_URL + static_files.js_path response = server.get(url, headers={"If-Modified-Since": last_mod}) assert "Content-Type" not in response.headers def test_get_nonascii_file(server, static_files, _collect_static): url = settings.STATIC_URL + static_files.nonascii_path response = server.get(url) assert response.content == static_files.nonascii_content @pytest.fixture(params=[True, False]) def finder_static_files(request): files = Files("static", js="app.js", index="with-index/index.html") with override_settings( STATICFILES_DIRS=[files.directory], WHITENOISE_USE_FINDERS=True, WHITENOISE_AUTOREFRESH=request.param, WHITENOISE_INDEX_FILE=True, STATIC_ROOT=None, ): finders.get_finder.cache_clear() yield files def test_no_content_disposition_header(server, static_files, _collect_static): url = settings.STATIC_URL + static_files.js_path response = server.get(url) assert response.headers.get("content-disposition") is None @pytest.fixture() def finder_application(finder_static_files): return get_wsgi_application() @pytest.fixture() def finder_server(finder_application): app_server = AppServer(finder_application) with closing(app_server): yield app_server def test_file_served_from_static_dir(finder_static_files, finder_server): url = settings.STATIC_URL + finder_static_files.js_path response = finder_server.get(url) assert response.content == finder_static_files.js_content def test_non_ascii_requests_safely_ignored(finder_server): response = finder_server.get(settings.STATIC_URL + "test\u263A") assert 404 == response.status_code def test_requests_for_directory_safely_ignored(finder_server): url = settings.STATIC_URL + "directory" response = finder_server.get(url) assert 404 == response.status_code def test_index_file_served_at_directory_path(finder_static_files, finder_server): path = finder_static_files.index_path.rpartition("/")[0] + "/" response = finder_server.get(settings.STATIC_URL + path) assert response.content == finder_static_files.index_content def test_index_file_path_redirected(finder_static_files, finder_server): directory_path = finder_static_files.index_path.rpartition("/")[0] + "/" index_url = settings.STATIC_URL + finder_static_files.index_path response = finder_server.get(index_url, allow_redirects=False) location = get_url_path(response.url, response.headers["Location"]) assert response.status_code == 302 assert location == settings.STATIC_URL + directory_path def test_directory_path_without_trailing_slash_redirected( finder_static_files, finder_server ): directory_path = finder_static_files.index_path.rpartition("/")[0] + "/" directory_url = settings.STATIC_URL + directory_path.rstrip("/") response = finder_server.get(directory_url, allow_redirects=False) location = get_url_path(response.url, response.headers["Location"]) assert response.status_code == 302 assert location == settings.STATIC_URL + directory_path def test_whitenoise_file_response_has_only_one_header(): response = WhiteNoiseFileResponse(open(__file__, "rb")) response.close() headers = {key.lower() for key, value in response.items()} # This subclass should have none of the default headers that FileReponse # sets 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) response = server.get(url) assert response.content == static_files.js_content whitenoise-6.0.0/tests/test_files/000077500000000000000000000000001420103656700172025ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/assets/000077500000000000000000000000001420103656700205045ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/assets/compressed.css000066400000000000000000000003321420103656700233600ustar00rootroot00000000000000body { background-color: white; color: black; } .thing { border: 1px solid black; color: blue; } .thing2 { border: 1px solid green; color: red; } .thing3 { border: 1px solid yellow; color: purple; } whitenoise-6.0.0/tests/test_files/assets/compressed.css.gz000066400000000000000000000002001420103656700237710ustar00rootroot00000000000000ңVuA 0}N1Pig!һlٸ^pq*%a3 >aBCs \L1E#ox +dTwÂ%ÔШF,ahGwhitenoise-6.0.0/tests/test_files/assets/custom-mime.foobar000066400000000000000000000000111420103656700241250ustar00rootroot00000000000000fizzbuzz whitenoise-6.0.0/tests/test_files/assets/subdir/000077500000000000000000000000001420103656700217745ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/assets/subdir/javascript.js000066400000000000000000000000431420103656700244750ustar00rootroot00000000000000var myFunction = { return 42; }; whitenoise-6.0.0/tests/test_files/assets/with-index/000077500000000000000000000000001420103656700225645ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/assets/with-index/index.html000066400000000000000000000000371420103656700245610ustar00rootroot00000000000000

Hello

whitenoise-6.0.0/tests/test_files/root/000077500000000000000000000000001420103656700201655ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/root/robots.txt000066400000000000000000000000141420103656700222310ustar00rootroot00000000000000Disallow: / whitenoise-6.0.0/tests/test_files/static/000077500000000000000000000000001420103656700204715ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/static/app.js000066400000000000000000000002411420103656700216040ustar00rootroot00000000000000var myFunction = function() { return 42; }; function thisFilesNeedsToBeBigEnoughToRequireGzipping(arg) { doAthing(); moreThings().then(evenMoreThings); } whitenoise-6.0.0/tests/test_files/static/directory/000077500000000000000000000000001420103656700224755ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/static/directory/.keep000066400000000000000000000000001420103656700234100ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/static/directory/pixel.gif000066400000000000000000000000531420103656700243030ustar00rootroot00000000000000GIF89a!,L;whitenoise-6.0.0/tests/test_files/static/nonascii✓.txt000066400000000000000000000000031420103656700240470ustar00rootroot00000000000000hi whitenoise-6.0.0/tests/test_files/static/styles.css000066400000000000000000000004121420103656700225230ustar00rootroot00000000000000body { background-color: white; color: black; } .thing { border: 1px solid black; color: blue; background-image: url('directory/pixel.gif'); } .thing2 { border: 1px solid green; color: red; } .thing3 { border: 1px solid yellow; color: purple; } whitenoise-6.0.0/tests/test_files/static/with-index/000077500000000000000000000000001420103656700225515ustar00rootroot00000000000000whitenoise-6.0.0/tests/test_files/static/with-index/index.html000066400000000000000000000000371420103656700245460ustar00rootroot00000000000000

Hello

whitenoise-6.0.0/tests/test_media_types.py000066400000000000000000000015131420103656700207550ustar00rootroot00000000000000from __future__ import annotations from whitenoise.media_types import MediaTypes def test_matched_filename(): result = MediaTypes().get_type("static/apple-app-site-association") assert result == "application/pkc7-mime" def test_matched_filename_cased(): result = MediaTypes().get_type("static/Apple-App-Site-Association") assert result == "application/pkc7-mime" def test_matched_extension(): result = MediaTypes().get_type("static/app.js") assert result == "text/javascript" def test_unmatched_extension(): result = MediaTypes().get_type("static/app.example-unmatched") assert result == "application/octet-stream" def test_extra_types(): types = MediaTypes(extra_types={".js": "application/javascript"}) result = types.get_type("static/app.js") assert result == "application/javascript" whitenoise-6.0.0/tests/test_runserver_nostatic.py000066400000000000000000000007561420103656700224210ustar00rootroot00000000000000from __future__ import annotations from django.core.management import get_commands, load_command_class def get_command_instance(name): app_name = get_commands()[name] return load_command_class(app_name, name) def test_command_output(): command = get_command_instance("runserver") parser = command.create_parser("manage.py", "runserver") assert "Wrapped by 'whitenoise.runserver_nostatic'" in parser.format_help() assert not parser.get_default("use_static_handler") whitenoise-6.0.0/tests/test_storage.py000066400000000000000000000060121420103656700201150ustar00rootroot00000000000000from __future__ import annotations import os import re import shutil import tempfile from posixpath import basename import pytest from django.conf import settings from django.contrib.staticfiles.storage import HashedFilesMixin, 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 @pytest.fixture() def setup(): staticfiles_storage._wrapped = empty files = Files("static") tmp = tempfile.mkdtemp() with override_settings( STATICFILES_DIRS=[files.directory], STATIC_ROOT=tmp, ): yield settings staticfiles_storage._wrapped = empty shutil.rmtree(tmp) @pytest.fixture() def _compressed_storage(setup): with override_settings( STATICFILES_STORAGE="whitenoise.storage.CompressedStaticFilesStorage" ): call_command("collectstatic", verbosity=0, interactive=False) @pytest.fixture() def _compressed_manifest_storage(setup): with override_settings( STATICFILES_STORAGE="whitenoise.storage.CompressedManifestStaticFilesStorage", WHITENOISE_KEEP_ONLY_HASHED_FILES=True, ): call_command("collectstatic", verbosity=0, interactive=False) def test_compressed_files_are_created(_compressed_storage): for name in ["styles.css.gz", "styles.css.br"]: path = os.path.join(settings.STATIC_ROOT, name) assert os.path.exists(path) def test_make_helpful_exception(_compressed_manifest_storage): class TriggerException(HashedFilesMixin): def exists(self, path): return False exception = None try: TriggerException().hashed_name("/missing/file.png") except ValueError as e: exception = e helpful_exception = HelpfulExceptionMixin().make_helpful_exception( exception, "styles/app.css" ) assert isinstance(helpful_exception, MissingFileError) def test_unversioned_files_are_deleted(_compressed_manifest_storage): name = "styles.css" versioned_url = staticfiles_storage.url(name) versioned_name = basename(versioned_url) name_pattern = re.compile("^" + name.replace(".", r"\.([0-9a-f]+\.)?") + "$") remaining_files = [ f for f in os.listdir(settings.STATIC_ROOT) if name_pattern.match(f) ] assert [versioned_name] == remaining_files def test_manifest_file_is_left_in_place(_compressed_manifest_storage): manifest_file = os.path.join(settings.STATIC_ROOT, "staticfiles.json") assert os.path.exists(manifest_file) def test_manifest_strict_attribute_is_set(): with override_settings(WHITENOISE_MANIFEST_STRICT=True): storage = CompressedManifestStaticFilesStorage() assert storage.manifest_strict is True with override_settings(WHITENOISE_MANIFEST_STRICT=False): storage = CompressedManifestStaticFilesStorage() assert storage.manifest_strict is False whitenoise-6.0.0/tests/test_string_utils.py000066400000000000000000000021231420103656700211760ustar00rootroot00000000000000from __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("