diff --git a/docs/base.rst b/docs/base.rst index b0b272ae..7b65b1b8 100644 --- a/docs/base.rst +++ b/docs/base.rst @@ -69,7 +69,7 @@ Usage is simple: .. code-block:: console $ python -m whitenoise.compress --help - usage: compress.py [-h] [-q] [--no-gzip] [--no-brotli] + usage: compress.py [-h] [-q] [--no-gzip] [--compressor-class COMPRESSOR_CLASS] [--no-brotli] root [extensions [extensions ...]] Search for all files inside *not* matching and produce @@ -87,6 +87,8 @@ Usage is simple: -q, --quiet Don't produce log output --no-gzip Don't produce gzip '.gz' files --no-brotli Don't produce brotli '.br' files + --compressor-class COMPRESSOR_CLASS + Custom compressor class to use (default: whitenoise.compress.Compressor) 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. diff --git a/docs/django.rst b/docs/django.rst index 3b6e6421..1fb6a442 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -389,6 +389,13 @@ arguments upper-cased with a 'WHITENOISE\_' prefix. just skip over them. +.. attribute:: WHITENOISE_COMPRESSOR_CLASS + + :default: ``'whitenoise.compress.Compressor'`` + + String with custom Compressor class dotted path. + + .. attribute:: WHITENOISE_ADD_HEADERS_FUNCTION :default: ``None`` diff --git a/src/whitenoise/compress.py b/src/whitenoise/compress.py index 143e1e44..53353f9d 100644 --- a/src/whitenoise/compress.py +++ b/src/whitenoise/compress.py @@ -6,6 +6,9 @@ import re from io import BytesIO +from django.conf import settings +from django.utils.module_loading import import_string + try: import brotli @@ -134,6 +137,16 @@ def write_data(self, path, data, suffix, stat_result): return filename +def get_compressor_class(): + return import_string( + getattr( + settings, + "WHITENOISE_COMPRESSOR_CLASS", + "whitenoise.compress.Compressor", + ), + ) + + def main(argv=None): parser = argparse.ArgumentParser( description="Search for all files inside *not* matching " @@ -150,6 +163,13 @@ def main(argv=None): action="store_false", dest="use_gzip", ) + parser.add_argument( + "--compressor-class", + help="Path to compressor class", + dest="compressor_class", + type=str, + default=None, + ) parser.add_argument( "--no-brotli", help="Don't produce brotli '.br' files", @@ -157,7 +177,12 @@ def main(argv=None): dest="use_brotli", ) parser.add_argument("root", help="Path root from which to search for files") - default_exclude = ", ".join(Compressor.SKIP_COMPRESS_EXTENSIONS) + compressor_class_str = parser.parse_args(argv).compressor_class + if compressor_class_str is None: + compressor_class = get_compressor_class() + else: + compressor_class = import_string(compressor_class_str) + default_exclude = ", ".join(compressor_class.SKIP_COMPRESS_EXTENSIONS) parser.add_argument( "extensions", nargs="*", @@ -165,11 +190,11 @@ def main(argv=None): "File extensions to exclude from compression " + f"(default: {default_exclude})" ), - default=Compressor.SKIP_COMPRESS_EXTENSIONS, + default=compressor_class.SKIP_COMPRESS_EXTENSIONS, ) args = parser.parse_args(argv) - compressor = Compressor( + compressor = compressor_class( extensions=args.extensions, use_gzip=args.use_gzip, use_brotli=args.use_brotli, diff --git a/src/whitenoise/storage.py b/src/whitenoise/storage.py index b7d357b6..d6645959 100644 --- a/src/whitenoise/storage.py +++ b/src/whitenoise/storage.py @@ -13,7 +13,8 @@ from django.contrib.staticfiles.storage import ManifestStaticFilesStorage from django.contrib.staticfiles.storage import StaticFilesStorage -from whitenoise.compress import Compressor +from .compress import Compressor +from .compress import get_compressor_class _PostProcessT = Iterator[Union[Tuple[str, str, bool], Tuple[str, None, RuntimeError]]] @@ -41,7 +42,7 @@ def post_process( yield path, compressed_name, True def create_compressor(self, **kwargs: Any) -> Compressor: - return Compressor(**kwargs) + return get_compressor_class()(**kwargs) class MissingFileError(ValueError): @@ -126,7 +127,7 @@ def delete_files(self, files_to_delete): raise def create_compressor(self, **kwargs): - return Compressor(**kwargs) + return get_compressor_class()(**kwargs) def compress_files(self, names): extensions = getattr(settings, "WHITENOISE_SKIP_COMPRESS_EXTENSIONS", None) diff --git a/tests/custom_compressor.py b/tests/custom_compressor.py new file mode 100644 index 00000000..22fc6e69 --- /dev/null +++ b/tests/custom_compressor.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from whitenoise.compress import Compressor + +called = False + + +class CustomCompressor(Compressor): + def compress(self, path): + global called + print("CALLLED") + called = True + return super().compress(path) diff --git a/tests/test_compress.py b/tests/test_compress.py index 11ea9471..b383df97 100644 --- a/tests/test_compress.py +++ b/tests/test_compress.py @@ -8,10 +8,13 @@ import tempfile import pytest +from django.test import override_settings from whitenoise.compress import Compressor from whitenoise.compress import main as compress_main +from . import custom_compressor + COMPRESSABLE_FILE = "application.css" TOO_SMALL_FILE = "too-small.css" WRONG_EXTENSION = "image.jpg" @@ -84,3 +87,59 @@ def test_compressed_effectively_no_orig_size(): assert not compressor.is_compressed_effectively( "test_encoding", "test_path", 0, "test_data" ) + + +def test_default_compressor(files_dir): + # Run the compression command with default compressor + custom_compressor.called = False + + compress_main([files_dir]) + + for path in TEST_FILES.keys(): + full_path = os.path.join(files_dir, path) + if path.endswith(".jpg"): + assert not os.path.exists(full_path + ".gz") + else: + if TOO_SMALL_FILE not in full_path: + assert os.path.exists(full_path + ".gz") + + assert custom_compressor.called is False + + +def test_custom_compressor(files_dir): + custom_compressor.called = False + + # Run the compression command with the custom compressor + compress_main( + [files_dir, "--compressor-class=tests.custom_compressor.CustomCompressor"] + ) + + assert custom_compressor.called is True + + for path in TEST_FILES.keys(): + full_path = os.path.join(files_dir, path) + if path.endswith(".jpg"): + assert not os.path.exists(full_path + ".gz") + else: + if TOO_SMALL_FILE not in full_path: + assert os.path.exists(full_path + ".gz") + + +@override_settings( + WHITENOISE_COMPRESSOR_CLASS="tests.custom_compressor.CustomCompressor" +) +def test_custom_compressor_settings(files_dir): + """Test if the custom compressor can be set via WHITENOISE_COMPRESSOR_CLASS settings""" + custom_compressor.called = False + + compress_main([files_dir]) + + assert custom_compressor.called is True + + for path in TEST_FILES.keys(): + full_path = os.path.join(files_dir, path) + if path.endswith(".jpg"): + assert not os.path.exists(full_path + ".gz") + else: + if TOO_SMALL_FILE not in full_path: + assert os.path.exists(full_path + ".gz")