Skip to content

Commit

Permalink
Fix Django "consume iterator" warning
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger committed Aug 26, 2024
1 parent f20ec38 commit 19dc135
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 8 deletions.
2 changes: 1 addition & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ nav:
- Reference:
- ServeStaticASGI: servestatic-asgi.md
- ServeStatic: servestatic.md
- Django Project:
- Django:
- Settings: django-settings.md
- FAQ: django-faq.md
- About:
Expand Down
35 changes: 28 additions & 7 deletions src/servestatic/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@

import django
from aiofiles.base import AiofilesContextManager
from asgiref.sync import iscoroutinefunction
from asgiref.sync import markcoroutinefunction
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
from django.conf import settings
from django.contrib.staticfiles import finders
from django.contrib.staticfiles.storage import staticfiles_storage
Expand Down Expand Up @@ -49,7 +48,6 @@ class AsyncServeStaticFileResponse(ServeStaticFileResponse):
def _set_streaming_content(self, value):
# Make sure the value is an async file handle
if not isinstance(value, AiofilesContextManager):
self.file_to_stream = None
return super()._set_streaming_content(value)

iterator = AsyncFileIterator(value)
Expand Down Expand Up @@ -167,10 +165,16 @@ def __init__(self, get_response, settings=settings):
def __call__(self, request):
if iscoroutinefunction(self.get_response):
return self.acall(request)
else:
return self.call(request)

# Force Django >= 3.2 to use async file responses
if django.VERSION >= (3, 2):
return asyncio.run(self.acall(request))

# Django version has no async uspport
return self.call(request)

def call(self, request):
"""TODO: This can be deleted once Django 4.2 is the minimum supported version."""
if self.autorefresh:
static_file = self.find_file(request.path_info)
else:
Expand All @@ -195,7 +199,10 @@ async def acall(self, request):
def serve(static_file, request):
response = static_file.get_response(request.method, request.META)
status = int(response.status)
http_response = ServeStaticFileResponse(response.file or (), status=status)
http_response = ServeStaticFileResponse(
response.file or (),
status=status,
)
# Remove default content-type
del http_response["content-type"]
for key, value in response.headers:
Expand All @@ -206,7 +213,10 @@ def serve(static_file, request):
async def aserve(static_file, request):
response = await static_file.aget_response(request.method, request.META)
status = int(response.status)
http_response = AsyncServeStaticFileResponse(response.file or (), status=status)
http_response = AsyncServeStaticFileResponse(
response.file or EmptyAsyncIterator(),
status=status,
)
# Remove default content-type
del http_response["content-type"]
for key, value in response.headers:
Expand Down Expand Up @@ -296,6 +306,17 @@ async def __aiter__(self):
yield chunk


class EmptyAsyncIterator:
"""Async iterator for responses that have no content. Prevents Django from
throwing "StreamingHttpResponse must consume synchronous iterators" warnings."""

def __aiter__(self):
return self

async def __anext__(self):
raise StopAsyncIteration


class AsyncToSyncIterator:
"""Converts any async iterator to sync as efficiently as possible while retaining
full compatibility with any environment.
Expand Down

0 comments on commit 19dc135

Please sign in to comment.