From 79e6c9eb3ee1cdb351b3f0c8e69108e3bd5d9ccd Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:06:56 -0700 Subject: [PATCH] Drop Django 3.2 and 4.1 --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 4 +++- pyproject.toml | 24 ---------------------- src/servestatic/middleware.py | 38 +++++++++-------------------------- src/servestatic/responders.py | 6 ++++++ tests/django_settings.py | 14 ++++++------- tests/test_storage.py | 26 +++++++++--------------- 7 files changed, 36 insertions(+), 78 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11e4abba..3bc220cc 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 f1227af3..2031d20e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet)! +### Changed + +- Drop Django 3.2 and 4.1 support. ## [2.1.1] - 2024-10-27 diff --git a/pyproject.toml b/pyproject.toml index 5b9f5d5e..de553713 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 b513d630..434e9cb8 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 f29c62d7..3deb307d 100644 --- a/src/servestatic/responders.py +++ b/src/servestatic/responders.py @@ -152,11 +152,14 @@ 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: + raise ValueError("Content-Length header is required for range requests") start, end = self.get_byte_range(range_header, size) if start >= end: return self.get_range_not_satisfiable_response(file_handle, size) @@ -171,11 +174,14 @@ 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: + raise ValueError("Content-Length header is required for range requests") 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 a32d1092..cdec60b7 100644 --- a/tests/django_settings.py +++ b/tests/django_settings.py @@ -19,14 +19,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 5144c0ae..c480f33a 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -36,15 +36,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 +50,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)