diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11e4abb..3bc220c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,4 +27,4 @@ repos: rev: "1.21.0" hooks: - id: django-upgrade - args: [--target-version, "3.2"] + args: [--target-version, "4.2"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae967f..ed7752e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Using the following categories, list your changes in this order: ### Changed +- Drop Django 3.2 and 4.1 support. - Any errors from threads in the `servestatic.compress` command are now raised. ## [2.1.1] - 2024-10-27 diff --git a/pyproject.toml b/pyproject.toml index 5b9f5d5..de55371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,21 +51,6 @@ extra-dependencies = ["pytest-sugar", "requests", "brotli"] randomize = true matrix-name-format = "{variable}-{value}" -# Django 3.2 -[[tool.hatch.envs.hatch-test.matrix]] -python = ["3.9", "3.10"] -django = ["3.2"] - -# Django 4.0 -[[tool.hatch.envs.hatch-test.matrix]] -python = ["3.9", "3.10"] -django = ["4.0"] - -# Django 4.1 -[[tool.hatch.envs.hatch-test.matrix]] -python = ["3.9", "3.10", "3.11"] -django = ["4.1"] - # Django 4.2 [[tool.hatch.envs.hatch-test.matrix]] python = ["3.9", "3.10", "3.11", "3.12"] @@ -83,15 +68,6 @@ django = ["5.1"] [tool.hatch.envs.hatch-test.overrides] matrix.django.dependencies = [ - { if = [ - "3.2", - ], value = "django~=3.2" }, - { if = [ - "4.0", - ], value = "django~=4.0" }, - { if = [ - "4.1", - ], value = "django~=4.1" }, { if = [ "4.2", ], value = "django~=4.2" }, diff --git a/src/servestatic/middleware.py b/src/servestatic/middleware.py index b513d63..434e9cb 100644 --- a/src/servestatic/middleware.py +++ b/src/servestatic/middleware.py @@ -7,7 +7,6 @@ from urllib.parse import urlparse from urllib.request import url2pathname -import django from asgiref.sync import iscoroutinefunction, markcoroutinefunction from django.conf import settings as django_settings from django.contrib.staticfiles import finders @@ -17,12 +16,7 @@ ) from django.http import FileResponse, HttpRequest -from servestatic.responders import ( - AsyncSlicedFile, - MissingFileError, - SlicedFile, - StaticFile, -) +from servestatic.responders import AsyncSlicedFile, MissingFileError, StaticFile from servestatic.utils import ( AsyncFile, AsyncFileIterator, @@ -253,31 +247,19 @@ def set_headers(self, *args, **kwargs): pass def _set_streaming_content(self, value): - # Django < 4.2 doesn't support async file responses, so we must perform - # some conversions to ensure compatibility. - if django.VERSION < (4, 2): - if isinstance(value, AsyncFile): - value = value.open_raw() - elif isinstance(value, EmptyAsyncIterator): - value = () - elif isinstance(value, AsyncSlicedFile): - value = SlicedFile(value.fileobj.open_raw(), value.start, value.end) - # Django 4.2+ supports async file responses, but they need to be converted from # a file-like object to an iterator, otherwise Django will assume the content is # a traditional (sync) file object. - elif isinstance(value, (AsyncFile, AsyncSlicedFile)): + if isinstance(value, (AsyncFile, AsyncSlicedFile)): value = AsyncFileIterator(value) super()._set_streaming_content(value) - if django.VERSION >= (4, 2): - - def __iter__(self): - """The way that Django 4.2+ converts async to sync is inefficient, so - we override it with a better implementation. Django only uses this method - when running via WSGI.""" - try: - return iter(self.streaming_content) - except TypeError: - return iter(AsyncToSyncIterator(self.streaming_content)) + def __iter__(self): + """The way that Django 4.2+ converts async to sync is inefficient, so + we override it with a better implementation. Django only uses this method + when running via WSGI.""" + try: + return iter(self.streaming_content) + except TypeError: + return iter(AsyncToSyncIterator(self.streaming_content)) diff --git a/src/servestatic/responders.py b/src/servestatic/responders.py index f29c62d..a75a192 100644 --- a/src/servestatic/responders.py +++ b/src/servestatic/responders.py @@ -152,11 +152,15 @@ async def aget_response(self, method, request_headers): def get_range_response(self, range_header, base_headers, file_handle): headers = [] + size: int | None = None for item in base_headers: if item[0] == "Content-Length": size = int(item[1]) else: headers.append(item) + if size is None: + msg = "Content-Length header is required for range requests" + raise ValueError(msg) start, end = self.get_byte_range(range_header, size) if start >= end: return self.get_range_not_satisfiable_response(file_handle, size) @@ -171,11 +175,15 @@ def get_range_response(self, range_header, base_headers, file_handle): async def aget_range_response(self, range_header, base_headers, file_handle): """Variant of `get_range_response` that works with async file objects.""" headers = [] + size: int | None = None for item in base_headers: if item[0] == "Content-Length": size = int(item[1]) else: headers.append(item) + if size is None: + msg = "Content-Length header is required for range requests" + raise ValueError(msg) start, end = self.get_byte_range(range_header, size) if start >= end: return await self.aget_range_not_satisfiable_response(file_handle, size) diff --git a/tests/django_settings.py b/tests/django_settings.py index a32d109..c1c029f 100644 --- a/tests/django_settings.py +++ b/tests/django_settings.py @@ -2,8 +2,6 @@ import os.path -import django - from .utils import TEST_FILE_PATH, AppServer ALLOWED_HOSTS = ["*"] @@ -19,14 +17,12 @@ STATIC_ROOT = os.path.join(TEST_FILE_PATH, "root") -if django.VERSION >= (4, 2): - STORAGES = { - "staticfiles": { - "BACKEND": "servestatic.storage.CompressedManifestStaticFilesStorage", - }, - } -else: - STATICFILES_STORAGE = "servestatic.storage.CompressedManifestStaticFilesStorage" +STORAGES = { + "staticfiles": { + "BACKEND": "servestatic.storage.CompressedManifestStaticFilesStorage", + }, +} + MIDDLEWARE = [ "tests.middleware.sync_middleware_1", diff --git a/tests/test_storage.py b/tests/test_storage.py index 5144c0a..3e8ce30 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -6,7 +6,6 @@ import tempfile from posixpath import basename -import django import pytest from django.conf import settings from django.contrib.staticfiles.storage import HashedFilesMixin, staticfiles_storage @@ -36,15 +35,12 @@ def setup(): @pytest.fixture def _compressed_storage(setup): backend = "servestatic.storage.CompressedStaticFilesStorage" - if django.VERSION >= (4, 2): - storages = { - "STORAGES": { - **settings.STORAGES, - "staticfiles": {"BACKEND": backend}, - } + storages = { + "STORAGES": { + **settings.STORAGES, + "staticfiles": {"BACKEND": backend}, } - else: - storages = {"STATICFILES_STORAGE": backend} + } with override_settings(**storages): yield @@ -53,15 +49,12 @@ def _compressed_storage(setup): @pytest.fixture def _compressed_manifest_storage(setup): backend = "servestatic.storage.CompressedManifestStaticFilesStorage" - if django.VERSION >= (4, 2): - storages = { - "STORAGES": { - **settings.STORAGES, - "staticfiles": {"BACKEND": backend}, - } + storages = { + "STORAGES": { + **settings.STORAGES, + "staticfiles": {"BACKEND": backend}, } - else: - storages = {"STATICFILES_STORAGE": backend} + } with override_settings(**storages, SERVESTATIC_KEEP_ONLY_HASHED_FILES=True): call_command("collectstatic", verbosity=0, interactive=False)