mirror of https://github.com/django/django.git
Fixed #31224 -- Added support for asynchronous views and middleware.
This implements support for asynchronous views, asynchronous tests, asynchronous middleware, and an asynchronous test client.
This commit is contained in:
parent
3f7e4b16bf
commit
fc0fa72ff4
|
@ -15,6 +15,7 @@ class SessionMiddleware(MiddlewareMixin):
|
||||||
def __init__(self, get_response=None):
|
def __init__(self, get_response=None):
|
||||||
self._get_response_none_deprecation(get_response)
|
self._get_response_none_deprecation(get_response)
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
self._async_check()
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
engine = import_module(settings.SESSION_ENGINE)
|
||||||
self.SessionStore = engine.SessionStore
|
self.SessionStore = engine.SessionStore
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -132,7 +131,7 @@ class ASGIHandler(base.BaseHandler):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.load_middleware()
|
self.load_middleware(is_async=True)
|
||||||
|
|
||||||
async def __call__(self, scope, receive, send):
|
async def __call__(self, scope, receive, send):
|
||||||
"""
|
"""
|
||||||
|
@ -158,12 +157,8 @@ class ASGIHandler(base.BaseHandler):
|
||||||
if request is None:
|
if request is None:
|
||||||
await self.send_response(error_response, send)
|
await self.send_response(error_response, send)
|
||||||
return
|
return
|
||||||
# Get the response, using a threadpool via sync_to_async, if needed.
|
# Get the response, using the async mode of BaseHandler.
|
||||||
if asyncio.iscoroutinefunction(self.get_response):
|
response = await self.get_response_async(request)
|
||||||
response = await self.get_response(request)
|
|
||||||
else:
|
|
||||||
# If get_response is synchronous, run it non-blocking.
|
|
||||||
response = await sync_to_async(self.get_response)(request)
|
|
||||||
response._handler_class = self.__class__
|
response._handler_class = self.__class__
|
||||||
# Increase chunk size on file responses (ASGI servers handles low-level
|
# Increase chunk size on file responses (ASGI servers handles low-level
|
||||||
# chunking).
|
# chunking).
|
||||||
|
@ -264,7 +259,7 @@ class ASGIHandler(base.BaseHandler):
|
||||||
'body': chunk,
|
'body': chunk,
|
||||||
'more_body': not last,
|
'more_body': not last,
|
||||||
})
|
})
|
||||||
response.close()
|
await sync_to_async(response.close)()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def chunk_bytes(cls, data):
|
def chunk_bytes(cls, data):
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import types
|
import types
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync, sync_to_async
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed
|
from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed
|
||||||
from django.core.signals import request_finished
|
from django.core.signals import request_finished
|
||||||
|
@ -20,7 +23,7 @@ class BaseHandler:
|
||||||
_exception_middleware = None
|
_exception_middleware = None
|
||||||
_middleware_chain = None
|
_middleware_chain = None
|
||||||
|
|
||||||
def load_middleware(self):
|
def load_middleware(self, is_async=False):
|
||||||
"""
|
"""
|
||||||
Populate middleware lists from settings.MIDDLEWARE.
|
Populate middleware lists from settings.MIDDLEWARE.
|
||||||
|
|
||||||
|
@ -30,10 +33,28 @@ class BaseHandler:
|
||||||
self._template_response_middleware = []
|
self._template_response_middleware = []
|
||||||
self._exception_middleware = []
|
self._exception_middleware = []
|
||||||
|
|
||||||
handler = convert_exception_to_response(self._get_response)
|
get_response = self._get_response_async if is_async else self._get_response
|
||||||
|
handler = convert_exception_to_response(get_response)
|
||||||
|
handler_is_async = is_async
|
||||||
for middleware_path in reversed(settings.MIDDLEWARE):
|
for middleware_path in reversed(settings.MIDDLEWARE):
|
||||||
middleware = import_string(middleware_path)
|
middleware = import_string(middleware_path)
|
||||||
|
middleware_can_sync = getattr(middleware, 'sync_capable', True)
|
||||||
|
middleware_can_async = getattr(middleware, 'async_capable', False)
|
||||||
|
if not middleware_can_sync and not middleware_can_async:
|
||||||
|
raise RuntimeError(
|
||||||
|
'Middleware %s must have at least one of '
|
||||||
|
'sync_capable/async_capable set to True.' % middleware_path
|
||||||
|
)
|
||||||
|
elif not handler_is_async and middleware_can_sync:
|
||||||
|
middleware_is_async = False
|
||||||
|
else:
|
||||||
|
middleware_is_async = middleware_can_async
|
||||||
try:
|
try:
|
||||||
|
# Adapt handler, if needed.
|
||||||
|
handler = self.adapt_method_mode(
|
||||||
|
middleware_is_async, handler, handler_is_async,
|
||||||
|
debug=settings.DEBUG, name='middleware %s' % middleware_path,
|
||||||
|
)
|
||||||
mw_instance = middleware(handler)
|
mw_instance = middleware(handler)
|
||||||
except MiddlewareNotUsed as exc:
|
except MiddlewareNotUsed as exc:
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
@ -49,24 +70,56 @@ class BaseHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(mw_instance, 'process_view'):
|
if hasattr(mw_instance, 'process_view'):
|
||||||
self._view_middleware.insert(0, mw_instance.process_view)
|
self._view_middleware.insert(
|
||||||
|
0,
|
||||||
|
self.adapt_method_mode(is_async, mw_instance.process_view),
|
||||||
|
)
|
||||||
if hasattr(mw_instance, 'process_template_response'):
|
if hasattr(mw_instance, 'process_template_response'):
|
||||||
self._template_response_middleware.append(mw_instance.process_template_response)
|
self._template_response_middleware.append(
|
||||||
|
self.adapt_method_mode(is_async, mw_instance.process_template_response),
|
||||||
|
)
|
||||||
if hasattr(mw_instance, 'process_exception'):
|
if hasattr(mw_instance, 'process_exception'):
|
||||||
self._exception_middleware.append(mw_instance.process_exception)
|
# The exception-handling stack is still always synchronous for
|
||||||
|
# now, so adapt that way.
|
||||||
|
self._exception_middleware.append(
|
||||||
|
self.adapt_method_mode(False, mw_instance.process_exception),
|
||||||
|
)
|
||||||
|
|
||||||
handler = convert_exception_to_response(mw_instance)
|
handler = convert_exception_to_response(mw_instance)
|
||||||
|
handler_is_async = middleware_is_async
|
||||||
|
|
||||||
|
# Adapt the top of the stack, if needed.
|
||||||
|
handler = self.adapt_method_mode(is_async, handler, handler_is_async)
|
||||||
# We only assign to this when initialization is complete as it is used
|
# We only assign to this when initialization is complete as it is used
|
||||||
# as a flag for initialization being complete.
|
# as a flag for initialization being complete.
|
||||||
self._middleware_chain = handler
|
self._middleware_chain = handler
|
||||||
|
|
||||||
def make_view_atomic(self, view):
|
def adapt_method_mode(
|
||||||
non_atomic_requests = getattr(view, '_non_atomic_requests', set())
|
self, is_async, method, method_is_async=None, debug=False, name=None,
|
||||||
for db in connections.all():
|
):
|
||||||
if db.settings_dict['ATOMIC_REQUESTS'] and db.alias not in non_atomic_requests:
|
"""
|
||||||
view = transaction.atomic(using=db.alias)(view)
|
Adapt a method to be in the correct "mode":
|
||||||
return view
|
- If is_async is False:
|
||||||
|
- Synchronous methods are left alone
|
||||||
|
- Asynchronous methods are wrapped with async_to_sync
|
||||||
|
- If is_async is True:
|
||||||
|
- Synchronous methods are wrapped with sync_to_async()
|
||||||
|
- Asynchronous methods are left alone
|
||||||
|
"""
|
||||||
|
if method_is_async is None:
|
||||||
|
method_is_async = asyncio.iscoroutinefunction(method)
|
||||||
|
if debug and not name:
|
||||||
|
name = name or 'method %s()' % method.__qualname__
|
||||||
|
if is_async:
|
||||||
|
if not method_is_async:
|
||||||
|
if debug:
|
||||||
|
logger.debug('Synchronous %s adapted.', name)
|
||||||
|
return sync_to_async(method, thread_sensitive=True)
|
||||||
|
elif method_is_async:
|
||||||
|
if debug:
|
||||||
|
logger.debug('Asynchronous %s adapted.' % name)
|
||||||
|
return async_to_sync(method)
|
||||||
|
return method
|
||||||
|
|
||||||
def get_response(self, request):
|
def get_response(self, request):
|
||||||
"""Return an HttpResponse object for the given HttpRequest."""
|
"""Return an HttpResponse object for the given HttpRequest."""
|
||||||
|
@ -82,6 +135,26 @@ class BaseHandler:
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
async def get_response_async(self, request):
|
||||||
|
"""
|
||||||
|
Asynchronous version of get_response.
|
||||||
|
|
||||||
|
Funneling everything, including WSGI, into a single async
|
||||||
|
get_response() is too slow. Avoid the context switch by using
|
||||||
|
a separate async response path.
|
||||||
|
"""
|
||||||
|
# Setup default url resolver for this thread.
|
||||||
|
set_urlconf(settings.ROOT_URLCONF)
|
||||||
|
response = await self._middleware_chain(request)
|
||||||
|
response._resource_closers.append(request.close)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
await sync_to_async(log_response)(
|
||||||
|
'%s: %s', response.reason_phrase, request.path,
|
||||||
|
response=response,
|
||||||
|
request=request,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
def _get_response(self, request):
|
def _get_response(self, request):
|
||||||
"""
|
"""
|
||||||
Resolve and call the view, then apply view, exception, and
|
Resolve and call the view, then apply view, exception, and
|
||||||
|
@ -89,17 +162,7 @@ class BaseHandler:
|
||||||
inside the request/response middleware.
|
inside the request/response middleware.
|
||||||
"""
|
"""
|
||||||
response = None
|
response = None
|
||||||
|
callback, callback_args, callback_kwargs = self.resolve_request(request)
|
||||||
if hasattr(request, 'urlconf'):
|
|
||||||
urlconf = request.urlconf
|
|
||||||
set_urlconf(urlconf)
|
|
||||||
resolver = get_resolver(urlconf)
|
|
||||||
else:
|
|
||||||
resolver = get_resolver()
|
|
||||||
|
|
||||||
resolver_match = resolver.resolve(request.path_info)
|
|
||||||
callback, callback_args, callback_kwargs = resolver_match
|
|
||||||
request.resolver_match = resolver_match
|
|
||||||
|
|
||||||
# Apply view middleware
|
# Apply view middleware
|
||||||
for middleware_method in self._view_middleware:
|
for middleware_method in self._view_middleware:
|
||||||
|
@ -109,6 +172,9 @@ class BaseHandler:
|
||||||
|
|
||||||
if response is None:
|
if response is None:
|
||||||
wrapped_callback = self.make_view_atomic(callback)
|
wrapped_callback = self.make_view_atomic(callback)
|
||||||
|
# If it is an asynchronous view, run it in a subthread.
|
||||||
|
if asyncio.iscoroutinefunction(wrapped_callback):
|
||||||
|
wrapped_callback = async_to_sync(wrapped_callback)
|
||||||
try:
|
try:
|
||||||
response = wrapped_callback(request, *callback_args, **callback_kwargs)
|
response = wrapped_callback(request, *callback_args, **callback_kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -137,6 +203,123 @@ class BaseHandler:
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
async def _get_response_async(self, request):
|
||||||
|
"""
|
||||||
|
Resolve and call the view, then apply view, exception, and
|
||||||
|
template_response middleware. This method is everything that happens
|
||||||
|
inside the request/response middleware.
|
||||||
|
"""
|
||||||
|
response = None
|
||||||
|
callback, callback_args, callback_kwargs = self.resolve_request(request)
|
||||||
|
|
||||||
|
# Apply view middleware.
|
||||||
|
for middleware_method in self._view_middleware:
|
||||||
|
response = await middleware_method(request, callback, callback_args, callback_kwargs)
|
||||||
|
if response:
|
||||||
|
break
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
wrapped_callback = self.make_view_atomic(callback)
|
||||||
|
# If it is a synchronous view, run it in a subthread
|
||||||
|
if not asyncio.iscoroutinefunction(wrapped_callback):
|
||||||
|
wrapped_callback = sync_to_async(wrapped_callback, thread_sensitive=True)
|
||||||
|
try:
|
||||||
|
response = await wrapped_callback(request, *callback_args, **callback_kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
response = await sync_to_async(
|
||||||
|
self.process_exception_by_middleware,
|
||||||
|
thread_sensitive=True,
|
||||||
|
)(e, request)
|
||||||
|
|
||||||
|
# Complain if the view returned None or an uncalled coroutine.
|
||||||
|
self.check_response(response, callback)
|
||||||
|
|
||||||
|
# If the response supports deferred rendering, apply template
|
||||||
|
# response middleware and then render the response
|
||||||
|
if hasattr(response, 'render') and callable(response.render):
|
||||||
|
for middleware_method in self._template_response_middleware:
|
||||||
|
response = await middleware_method(request, response)
|
||||||
|
# Complain if the template response middleware returned None or
|
||||||
|
# an uncalled coroutine.
|
||||||
|
self.check_response(
|
||||||
|
response,
|
||||||
|
middleware_method,
|
||||||
|
name='%s.process_template_response' % (
|
||||||
|
middleware_method.__self__.__class__.__name__,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if asyncio.iscoroutinefunction(response.render):
|
||||||
|
response = await response.render()
|
||||||
|
else:
|
||||||
|
response = await sync_to_async(response.render, thread_sensitive=True)()
|
||||||
|
except Exception as e:
|
||||||
|
response = await sync_to_async(
|
||||||
|
self.process_exception_by_middleware,
|
||||||
|
thread_sensitive=True,
|
||||||
|
)(e, request)
|
||||||
|
|
||||||
|
# Make sure the response is not a coroutine
|
||||||
|
if asyncio.iscoroutine(response):
|
||||||
|
raise RuntimeError('Response is still a coroutine.')
|
||||||
|
return response
|
||||||
|
|
||||||
|
def resolve_request(self, request):
|
||||||
|
"""
|
||||||
|
Retrieve/set the urlconf for the request. Return the view resolved,
|
||||||
|
with its args and kwargs.
|
||||||
|
"""
|
||||||
|
# Work out the resolver.
|
||||||
|
if hasattr(request, 'urlconf'):
|
||||||
|
urlconf = request.urlconf
|
||||||
|
set_urlconf(urlconf)
|
||||||
|
resolver = get_resolver(urlconf)
|
||||||
|
else:
|
||||||
|
resolver = get_resolver()
|
||||||
|
# Resolve the view, and assign the match object back to the request.
|
||||||
|
resolver_match = resolver.resolve(request.path_info)
|
||||||
|
request.resolver_match = resolver_match
|
||||||
|
return resolver_match
|
||||||
|
|
||||||
|
def check_response(self, response, callback, name=None):
|
||||||
|
"""
|
||||||
|
Raise an error if the view returned None or an uncalled coroutine.
|
||||||
|
"""
|
||||||
|
if not(response is None or asyncio.iscoroutine(response)):
|
||||||
|
return
|
||||||
|
if not name:
|
||||||
|
if isinstance(callback, types.FunctionType): # FBV
|
||||||
|
name = 'The view %s.%s' % (callback.__module__, callback.__name__)
|
||||||
|
else: # CBV
|
||||||
|
name = 'The view %s.%s.__call__' % (
|
||||||
|
callback.__module__,
|
||||||
|
callback.__class__.__name__,
|
||||||
|
)
|
||||||
|
if response is None:
|
||||||
|
raise ValueError(
|
||||||
|
"%s didn't return an HttpResponse object. It returned None "
|
||||||
|
"instead." % name
|
||||||
|
)
|
||||||
|
elif asyncio.iscoroutine(response):
|
||||||
|
raise ValueError(
|
||||||
|
"%s didn't return an HttpResponse object. It returned an "
|
||||||
|
"unawaited coroutine instead. You may need to add an 'await' "
|
||||||
|
"into your view." % name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Other utility methods.
|
||||||
|
|
||||||
|
def make_view_atomic(self, view):
|
||||||
|
non_atomic_requests = getattr(view, '_non_atomic_requests', set())
|
||||||
|
for db in connections.all():
|
||||||
|
if db.settings_dict['ATOMIC_REQUESTS'] and db.alias not in non_atomic_requests:
|
||||||
|
if asyncio.iscoroutinefunction(view):
|
||||||
|
raise RuntimeError(
|
||||||
|
'You cannot use ATOMIC_REQUESTS with async views.'
|
||||||
|
)
|
||||||
|
view = transaction.atomic(using=db.alias)(view)
|
||||||
|
return view
|
||||||
|
|
||||||
def process_exception_by_middleware(self, exception, request):
|
def process_exception_by_middleware(self, exception, request):
|
||||||
"""
|
"""
|
||||||
Pass the exception to the exception middleware. If no middleware
|
Pass the exception to the exception middleware. If no middleware
|
||||||
|
@ -148,23 +331,6 @@ class BaseHandler:
|
||||||
return response
|
return response
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def check_response(self, response, callback, name=None):
|
|
||||||
"""Raise an error if the view returned None."""
|
|
||||||
if response is not None:
|
|
||||||
return
|
|
||||||
if not name:
|
|
||||||
if isinstance(callback, types.FunctionType): # FBV
|
|
||||||
name = 'The view %s.%s' % (callback.__module__, callback.__name__)
|
|
||||||
else: # CBV
|
|
||||||
name = 'The view %s.%s.__call__' % (
|
|
||||||
callback.__module__,
|
|
||||||
callback.__class__.__name__,
|
|
||||||
)
|
|
||||||
raise ValueError(
|
|
||||||
"%s didn't return an HttpResponse object. It returned None "
|
|
||||||
"instead." % name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def reset_urlconf(sender, **kwargs):
|
def reset_urlconf(sender, **kwargs):
|
||||||
"""Reset the URLconf after each request is finished."""
|
"""Reset the URLconf after each request is finished."""
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import signals
|
from django.core import signals
|
||||||
from django.core.exceptions import (
|
from django.core.exceptions import (
|
||||||
|
@ -28,14 +31,24 @@ def convert_exception_to_response(get_response):
|
||||||
no middleware leaks an exception and that the next middleware in the stack
|
no middleware leaks an exception and that the next middleware in the stack
|
||||||
can rely on getting a response instead of an exception.
|
can rely on getting a response instead of an exception.
|
||||||
"""
|
"""
|
||||||
@wraps(get_response)
|
if asyncio.iscoroutinefunction(get_response):
|
||||||
def inner(request):
|
@wraps(get_response)
|
||||||
try:
|
async def inner(request):
|
||||||
response = get_response(request)
|
try:
|
||||||
except Exception as exc:
|
response = await get_response(request)
|
||||||
response = response_for_exception(request, exc)
|
except Exception as exc:
|
||||||
return response
|
response = await sync_to_async(response_for_exception)(request, exc)
|
||||||
return inner
|
return response
|
||||||
|
return inner
|
||||||
|
else:
|
||||||
|
@wraps(get_response)
|
||||||
|
def inner(request):
|
||||||
|
try:
|
||||||
|
response = get_response(request)
|
||||||
|
except Exception as exc:
|
||||||
|
response = response_for_exception(request, exc)
|
||||||
|
return response
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
def response_for_exception(request, exc):
|
def response_for_exception(request, exc):
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
"""Django Unit Test framework."""
|
"""Django Unit Test framework."""
|
||||||
|
|
||||||
from django.test.client import Client, RequestFactory
|
from django.test.client import (
|
||||||
|
AsyncClient, AsyncRequestFactory, Client, RequestFactory,
|
||||||
|
)
|
||||||
from django.test.testcases import (
|
from django.test.testcases import (
|
||||||
LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase,
|
LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase,
|
||||||
skipIfDBFeature, skipUnlessAnyDBFeature, skipUnlessDBFeature,
|
skipIfDBFeature, skipUnlessAnyDBFeature, skipUnlessDBFeature,
|
||||||
|
@ -11,8 +13,9 @@ from django.test.utils import (
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Client', 'RequestFactory', 'TestCase', 'TransactionTestCase',
|
'AsyncClient', 'AsyncRequestFactory', 'Client', 'RequestFactory',
|
||||||
'SimpleTestCase', 'LiveServerTestCase', 'skipIfDBFeature',
|
'TestCase', 'TransactionTestCase', 'SimpleTestCase', 'LiveServerTestCase',
|
||||||
'skipUnlessAnyDBFeature', 'skipUnlessDBFeature', 'ignore_warnings',
|
'skipIfDBFeature', 'skipUnlessAnyDBFeature', 'skipUnlessDBFeature',
|
||||||
'modify_settings', 'override_settings', 'override_system_checks', 'tag',
|
'ignore_warnings', 'modify_settings', 'override_settings',
|
||||||
|
'override_system_checks', 'tag',
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,7 +9,10 @@ from importlib import import_module
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit
|
from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit
|
||||||
|
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.handlers.asgi import ASGIRequest
|
||||||
from django.core.handlers.base import BaseHandler
|
from django.core.handlers.base import BaseHandler
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
@ -157,6 +160,52 @@ class ClientHandler(BaseHandler):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncClientHandler(BaseHandler):
|
||||||
|
"""An async version of ClientHandler."""
|
||||||
|
def __init__(self, enforce_csrf_checks=True, *args, **kwargs):
|
||||||
|
self.enforce_csrf_checks = enforce_csrf_checks
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
async def __call__(self, scope):
|
||||||
|
# Set up middleware if needed. We couldn't do this earlier, because
|
||||||
|
# settings weren't available.
|
||||||
|
if self._middleware_chain is None:
|
||||||
|
self.load_middleware(is_async=True)
|
||||||
|
# Extract body file from the scope, if provided.
|
||||||
|
if '_body_file' in scope:
|
||||||
|
body_file = scope.pop('_body_file')
|
||||||
|
else:
|
||||||
|
body_file = FakePayload('')
|
||||||
|
|
||||||
|
request_started.disconnect(close_old_connections)
|
||||||
|
await sync_to_async(request_started.send)(sender=self.__class__, scope=scope)
|
||||||
|
request_started.connect(close_old_connections)
|
||||||
|
request = ASGIRequest(scope, body_file)
|
||||||
|
# Sneaky little hack so that we can easily get round
|
||||||
|
# CsrfViewMiddleware. This makes life easier, and is probably required
|
||||||
|
# for backwards compatibility with external tests against admin views.
|
||||||
|
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
|
||||||
|
# Request goes through middleware.
|
||||||
|
response = await self.get_response_async(request)
|
||||||
|
# Simulate behaviors of most Web servers.
|
||||||
|
conditional_content_removal(request, response)
|
||||||
|
# Attach the originating ASGI request to the response so that it could
|
||||||
|
# be later retrieved.
|
||||||
|
response.asgi_request = request
|
||||||
|
# Emulate a server by calling the close method on completion.
|
||||||
|
if response.streaming:
|
||||||
|
response.streaming_content = await sync_to_async(closing_iterator_wrapper)(
|
||||||
|
response.streaming_content,
|
||||||
|
response.close,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
request_finished.disconnect(close_old_connections)
|
||||||
|
# Will fire request_finished.
|
||||||
|
await sync_to_async(response.close)()
|
||||||
|
request_finished.connect(close_old_connections)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def store_rendered_templates(store, signal, sender, template, context, **kwargs):
|
def store_rendered_templates(store, signal, sender, template, context, **kwargs):
|
||||||
"""
|
"""
|
||||||
Store templates and contexts that are rendered.
|
Store templates and contexts that are rendered.
|
||||||
|
@ -421,7 +470,194 @@ class RequestFactory:
|
||||||
return self.request(**r)
|
return self.request(**r)
|
||||||
|
|
||||||
|
|
||||||
class Client(RequestFactory):
|
class AsyncRequestFactory(RequestFactory):
|
||||||
|
"""
|
||||||
|
Class that lets you create mock ASGI-like Request objects for use in
|
||||||
|
testing. Usage:
|
||||||
|
|
||||||
|
rf = AsyncRequestFactory()
|
||||||
|
get_request = await rf.get('/hello/')
|
||||||
|
post_request = await rf.post('/submit/', {'foo': 'bar'})
|
||||||
|
|
||||||
|
Once you have a request object you can pass it to any view function,
|
||||||
|
including synchronous ones. The reason we have a separate class here is:
|
||||||
|
a) this makes ASGIRequest subclasses, and
|
||||||
|
b) AsyncTestClient can subclass it.
|
||||||
|
"""
|
||||||
|
def _base_scope(self, **request):
|
||||||
|
"""The base scope for a request."""
|
||||||
|
# This is a minimal valid ASGI scope, plus:
|
||||||
|
# - headers['cookie'] for cookie support,
|
||||||
|
# - 'client' often useful, see #8551.
|
||||||
|
scope = {
|
||||||
|
'asgi': {'version': '3.0'},
|
||||||
|
'type': 'http',
|
||||||
|
'http_version': '1.1',
|
||||||
|
'client': ['127.0.0.1', 0],
|
||||||
|
'server': ('testserver', '80'),
|
||||||
|
'scheme': 'http',
|
||||||
|
'method': 'GET',
|
||||||
|
'headers': [],
|
||||||
|
**self.defaults,
|
||||||
|
**request,
|
||||||
|
}
|
||||||
|
scope['headers'].append((
|
||||||
|
b'cookie',
|
||||||
|
b'; '.join(sorted(
|
||||||
|
('%s=%s' % (morsel.key, morsel.coded_value)).encode('ascii')
|
||||||
|
for morsel in self.cookies.values()
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
return scope
|
||||||
|
|
||||||
|
def request(self, **request):
|
||||||
|
"""Construct a generic request object."""
|
||||||
|
# This is synchronous, which means all methods on this class are.
|
||||||
|
# AsyncClient, however, has an async request function, which makes all
|
||||||
|
# its methods async.
|
||||||
|
if '_body_file' in request:
|
||||||
|
body_file = request.pop('_body_file')
|
||||||
|
else:
|
||||||
|
body_file = FakePayload('')
|
||||||
|
return ASGIRequest(self._base_scope(**request), body_file)
|
||||||
|
|
||||||
|
def generic(
|
||||||
|
self, method, path, data='', content_type='application/octet-stream',
|
||||||
|
secure=False, **extra,
|
||||||
|
):
|
||||||
|
"""Construct an arbitrary HTTP request."""
|
||||||
|
parsed = urlparse(str(path)) # path can be lazy.
|
||||||
|
data = force_bytes(data, settings.DEFAULT_CHARSET)
|
||||||
|
s = {
|
||||||
|
'method': method,
|
||||||
|
'path': self._get_path(parsed),
|
||||||
|
'server': ('127.0.0.1', '443' if secure else '80'),
|
||||||
|
'scheme': 'https' if secure else 'http',
|
||||||
|
'headers': [(b'host', b'testserver')],
|
||||||
|
}
|
||||||
|
if data:
|
||||||
|
s['headers'].extend([
|
||||||
|
(b'content-length', bytes(len(data))),
|
||||||
|
(b'content-type', content_type.encode('ascii')),
|
||||||
|
])
|
||||||
|
s['_body_file'] = FakePayload(data)
|
||||||
|
s.update(extra)
|
||||||
|
# If QUERY_STRING is absent or empty, we want to extract it from the
|
||||||
|
# URL.
|
||||||
|
if not s.get('query_string'):
|
||||||
|
s['query_string'] = parsed[4]
|
||||||
|
return self.request(**s)
|
||||||
|
|
||||||
|
|
||||||
|
class ClientMixin:
|
||||||
|
"""
|
||||||
|
Mixin with common methods between Client and AsyncClient.
|
||||||
|
"""
|
||||||
|
def store_exc_info(self, **kwargs):
|
||||||
|
"""Store exceptions when they are generated by a view."""
|
||||||
|
self.exc_info = sys.exc_info()
|
||||||
|
|
||||||
|
def check_exception(self, response):
|
||||||
|
"""
|
||||||
|
Look for a signaled exception, clear the current context exception
|
||||||
|
data, re-raise the signaled exception, and clear the signaled exception
|
||||||
|
from the local cache.
|
||||||
|
"""
|
||||||
|
response.exc_info = self.exc_info
|
||||||
|
if self.exc_info:
|
||||||
|
_, exc_value, _ = self.exc_info
|
||||||
|
self.exc_info = None
|
||||||
|
if self.raise_request_exception:
|
||||||
|
raise exc_value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self):
|
||||||
|
"""Return the current session variables."""
|
||||||
|
engine = import_module(settings.SESSION_ENGINE)
|
||||||
|
cookie = self.cookies.get(settings.SESSION_COOKIE_NAME)
|
||||||
|
if cookie:
|
||||||
|
return engine.SessionStore(cookie.value)
|
||||||
|
session = engine.SessionStore()
|
||||||
|
session.save()
|
||||||
|
self.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
|
||||||
|
return session
|
||||||
|
|
||||||
|
def login(self, **credentials):
|
||||||
|
"""
|
||||||
|
Set the Factory to appear as if it has successfully logged into a site.
|
||||||
|
|
||||||
|
Return True if login is possible or False if the provided credentials
|
||||||
|
are incorrect.
|
||||||
|
"""
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
user = authenticate(**credentials)
|
||||||
|
if user:
|
||||||
|
self._login(user)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def force_login(self, user, backend=None):
|
||||||
|
def get_backend():
|
||||||
|
from django.contrib.auth import load_backend
|
||||||
|
for backend_path in settings.AUTHENTICATION_BACKENDS:
|
||||||
|
backend = load_backend(backend_path)
|
||||||
|
if hasattr(backend, 'get_user'):
|
||||||
|
return backend_path
|
||||||
|
|
||||||
|
if backend is None:
|
||||||
|
backend = get_backend()
|
||||||
|
user.backend = backend
|
||||||
|
self._login(user, backend)
|
||||||
|
|
||||||
|
def _login(self, user, backend=None):
|
||||||
|
from django.contrib.auth import login
|
||||||
|
# Create a fake request to store login details.
|
||||||
|
request = HttpRequest()
|
||||||
|
if self.session:
|
||||||
|
request.session = self.session
|
||||||
|
else:
|
||||||
|
engine = import_module(settings.SESSION_ENGINE)
|
||||||
|
request.session = engine.SessionStore()
|
||||||
|
login(request, user, backend)
|
||||||
|
# Save the session values.
|
||||||
|
request.session.save()
|
||||||
|
# Set the cookie to represent the session.
|
||||||
|
session_cookie = settings.SESSION_COOKIE_NAME
|
||||||
|
self.cookies[session_cookie] = request.session.session_key
|
||||||
|
cookie_data = {
|
||||||
|
'max-age': None,
|
||||||
|
'path': '/',
|
||||||
|
'domain': settings.SESSION_COOKIE_DOMAIN,
|
||||||
|
'secure': settings.SESSION_COOKIE_SECURE or None,
|
||||||
|
'expires': None,
|
||||||
|
}
|
||||||
|
self.cookies[session_cookie].update(cookie_data)
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
"""Log out the user by removing the cookies and session object."""
|
||||||
|
from django.contrib.auth import get_user, logout
|
||||||
|
request = HttpRequest()
|
||||||
|
if self.session:
|
||||||
|
request.session = self.session
|
||||||
|
request.user = get_user(request)
|
||||||
|
else:
|
||||||
|
engine = import_module(settings.SESSION_ENGINE)
|
||||||
|
request.session = engine.SessionStore()
|
||||||
|
logout(request)
|
||||||
|
self.cookies = SimpleCookie()
|
||||||
|
|
||||||
|
def _parse_json(self, response, **extra):
|
||||||
|
if not hasattr(response, '_json'):
|
||||||
|
if not JSON_CONTENT_TYPE_RE.match(response.get('Content-Type')):
|
||||||
|
raise ValueError(
|
||||||
|
'Content-Type header is "%s", not "application/json"'
|
||||||
|
% response.get('Content-Type')
|
||||||
|
)
|
||||||
|
response._json = json.loads(response.content.decode(response.charset), **extra)
|
||||||
|
return response._json
|
||||||
|
|
||||||
|
|
||||||
|
class Client(ClientMixin, RequestFactory):
|
||||||
"""
|
"""
|
||||||
A class that can act as a client for testing purposes.
|
A class that can act as a client for testing purposes.
|
||||||
|
|
||||||
|
@ -446,23 +682,6 @@ class Client(RequestFactory):
|
||||||
self.exc_info = None
|
self.exc_info = None
|
||||||
self.extra = None
|
self.extra = None
|
||||||
|
|
||||||
def store_exc_info(self, **kwargs):
|
|
||||||
"""Store exceptions when they are generated by a view."""
|
|
||||||
self.exc_info = sys.exc_info()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def session(self):
|
|
||||||
"""Return the current session variables."""
|
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
|
||||||
cookie = self.cookies.get(settings.SESSION_COOKIE_NAME)
|
|
||||||
if cookie:
|
|
||||||
return engine.SessionStore(cookie.value)
|
|
||||||
|
|
||||||
session = engine.SessionStore()
|
|
||||||
session.save()
|
|
||||||
self.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
|
|
||||||
return session
|
|
||||||
|
|
||||||
def request(self, **request):
|
def request(self, **request):
|
||||||
"""
|
"""
|
||||||
The master request method. Compose the environment dictionary and pass
|
The master request method. Compose the environment dictionary and pass
|
||||||
|
@ -486,15 +705,8 @@ class Client(RequestFactory):
|
||||||
finally:
|
finally:
|
||||||
signals.template_rendered.disconnect(dispatch_uid=signal_uid)
|
signals.template_rendered.disconnect(dispatch_uid=signal_uid)
|
||||||
got_request_exception.disconnect(dispatch_uid=exception_uid)
|
got_request_exception.disconnect(dispatch_uid=exception_uid)
|
||||||
# Look for a signaled exception, clear the current context exception
|
# Check for signaled exceptions.
|
||||||
# data, then re-raise the signaled exception. Also clear the signaled
|
self.check_exception(response)
|
||||||
# exception from the local cache.
|
|
||||||
response.exc_info = self.exc_info
|
|
||||||
if self.exc_info:
|
|
||||||
_, exc_value, _ = self.exc_info
|
|
||||||
self.exc_info = None
|
|
||||||
if self.raise_request_exception:
|
|
||||||
raise exc_value
|
|
||||||
# Save the client and request that stimulated the response.
|
# Save the client and request that stimulated the response.
|
||||||
response.client = self
|
response.client = self
|
||||||
response.request = request
|
response.request = request
|
||||||
|
@ -583,85 +795,6 @@ class Client(RequestFactory):
|
||||||
response = self._handle_redirects(response, data=data, **extra)
|
response = self._handle_redirects(response, data=data, **extra)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def login(self, **credentials):
|
|
||||||
"""
|
|
||||||
Set the Factory to appear as if it has successfully logged into a site.
|
|
||||||
|
|
||||||
Return True if login is possible; False if the provided credentials
|
|
||||||
are incorrect.
|
|
||||||
"""
|
|
||||||
from django.contrib.auth import authenticate
|
|
||||||
user = authenticate(**credentials)
|
|
||||||
if user:
|
|
||||||
self._login(user)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def force_login(self, user, backend=None):
|
|
||||||
def get_backend():
|
|
||||||
from django.contrib.auth import load_backend
|
|
||||||
for backend_path in settings.AUTHENTICATION_BACKENDS:
|
|
||||||
backend = load_backend(backend_path)
|
|
||||||
if hasattr(backend, 'get_user'):
|
|
||||||
return backend_path
|
|
||||||
if backend is None:
|
|
||||||
backend = get_backend()
|
|
||||||
user.backend = backend
|
|
||||||
self._login(user, backend)
|
|
||||||
|
|
||||||
def _login(self, user, backend=None):
|
|
||||||
from django.contrib.auth import login
|
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
|
||||||
|
|
||||||
# Create a fake request to store login details.
|
|
||||||
request = HttpRequest()
|
|
||||||
|
|
||||||
if self.session:
|
|
||||||
request.session = self.session
|
|
||||||
else:
|
|
||||||
request.session = engine.SessionStore()
|
|
||||||
login(request, user, backend)
|
|
||||||
|
|
||||||
# Save the session values.
|
|
||||||
request.session.save()
|
|
||||||
|
|
||||||
# Set the cookie to represent the session.
|
|
||||||
session_cookie = settings.SESSION_COOKIE_NAME
|
|
||||||
self.cookies[session_cookie] = request.session.session_key
|
|
||||||
cookie_data = {
|
|
||||||
'max-age': None,
|
|
||||||
'path': '/',
|
|
||||||
'domain': settings.SESSION_COOKIE_DOMAIN,
|
|
||||||
'secure': settings.SESSION_COOKIE_SECURE or None,
|
|
||||||
'expires': None,
|
|
||||||
}
|
|
||||||
self.cookies[session_cookie].update(cookie_data)
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
"""Log out the user by removing the cookies and session object."""
|
|
||||||
from django.contrib.auth import get_user, logout
|
|
||||||
|
|
||||||
request = HttpRequest()
|
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
|
||||||
if self.session:
|
|
||||||
request.session = self.session
|
|
||||||
request.user = get_user(request)
|
|
||||||
else:
|
|
||||||
request.session = engine.SessionStore()
|
|
||||||
logout(request)
|
|
||||||
self.cookies = SimpleCookie()
|
|
||||||
|
|
||||||
def _parse_json(self, response, **extra):
|
|
||||||
if not hasattr(response, '_json'):
|
|
||||||
if not JSON_CONTENT_TYPE_RE.match(response.get('Content-Type')):
|
|
||||||
raise ValueError(
|
|
||||||
'Content-Type header is "{}", not "application/json"'
|
|
||||||
.format(response.get('Content-Type'))
|
|
||||||
)
|
|
||||||
response._json = json.loads(response.content.decode(response.charset), **extra)
|
|
||||||
return response._json
|
|
||||||
|
|
||||||
def _handle_redirects(self, response, data='', content_type='', **extra):
|
def _handle_redirects(self, response, data='', content_type='', **extra):
|
||||||
"""
|
"""
|
||||||
Follow any redirects by requesting responses from the server using GET.
|
Follow any redirects by requesting responses from the server using GET.
|
||||||
|
@ -714,3 +847,66 @@ class Client(RequestFactory):
|
||||||
raise RedirectCycleError("Too many redirects.", last_response=response)
|
raise RedirectCycleError("Too many redirects.", last_response=response)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncClient(ClientMixin, AsyncRequestFactory):
|
||||||
|
"""
|
||||||
|
An async version of Client that creates ASGIRequests and calls through an
|
||||||
|
async request path.
|
||||||
|
|
||||||
|
Does not currently support "follow" on its methods.
|
||||||
|
"""
|
||||||
|
def __init__(self, enforce_csrf_checks=False, raise_request_exception=True, **defaults):
|
||||||
|
super().__init__(**defaults)
|
||||||
|
self.handler = AsyncClientHandler(enforce_csrf_checks)
|
||||||
|
self.raise_request_exception = raise_request_exception
|
||||||
|
self.exc_info = None
|
||||||
|
self.extra = None
|
||||||
|
|
||||||
|
async def request(self, **request):
|
||||||
|
"""
|
||||||
|
The master request method. Compose the scope dictionary and pass to the
|
||||||
|
handler, return the result of the handler. Assume defaults for the
|
||||||
|
query environment, which can be overridden using the arguments to the
|
||||||
|
request.
|
||||||
|
"""
|
||||||
|
if 'follow' in request:
|
||||||
|
raise NotImplementedError(
|
||||||
|
'AsyncClient request methods do not accept the follow '
|
||||||
|
'parameter.'
|
||||||
|
)
|
||||||
|
scope = self._base_scope(**request)
|
||||||
|
# Curry a data dictionary into an instance of the template renderer
|
||||||
|
# callback function.
|
||||||
|
data = {}
|
||||||
|
on_template_render = partial(store_rendered_templates, data)
|
||||||
|
signal_uid = 'template-render-%s' % id(request)
|
||||||
|
signals.template_rendered.connect(on_template_render, dispatch_uid=signal_uid)
|
||||||
|
# Capture exceptions created by the handler.
|
||||||
|
exception_uid = 'request-exception-%s' % id(request)
|
||||||
|
got_request_exception.connect(self.store_exc_info, dispatch_uid=exception_uid)
|
||||||
|
try:
|
||||||
|
response = await self.handler(scope)
|
||||||
|
finally:
|
||||||
|
signals.template_rendered.disconnect(dispatch_uid=signal_uid)
|
||||||
|
got_request_exception.disconnect(dispatch_uid=exception_uid)
|
||||||
|
# Check for signaled exceptions.
|
||||||
|
self.check_exception(response)
|
||||||
|
# Save the client and request that stimulated the response.
|
||||||
|
response.client = self
|
||||||
|
response.request = request
|
||||||
|
# Add any rendered template detail to the response.
|
||||||
|
response.templates = data.get('templates', [])
|
||||||
|
response.context = data.get('context')
|
||||||
|
response.json = partial(self._parse_json, response)
|
||||||
|
# Attach the ResolverMatch instance to the response.
|
||||||
|
response.resolver_match = SimpleLazyObject(lambda: resolve(request['path']))
|
||||||
|
# Flatten a single context. Not really necessary anymore thanks to the
|
||||||
|
# __getattr__ flattening in ContextList, but has some edge case
|
||||||
|
# backwards compatibility implications.
|
||||||
|
if response.context and len(response.context) == 1:
|
||||||
|
response.context = response.context[0]
|
||||||
|
# Update persistent cookie data.
|
||||||
|
if response.cookies:
|
||||||
|
self.cookies.update(response.cookies)
|
||||||
|
return response
|
||||||
|
|
|
@ -33,7 +33,7 @@ from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
|
||||||
from django.forms.fields import CharField
|
from django.forms.fields import CharField
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.http.request import split_domain_port, validate_host
|
from django.http.request import split_domain_port, validate_host
|
||||||
from django.test.client import Client
|
from django.test.client import AsyncClient, Client
|
||||||
from django.test.html import HTMLParseError, parse_html
|
from django.test.html import HTMLParseError, parse_html
|
||||||
from django.test.signals import setting_changed, template_rendered
|
from django.test.signals import setting_changed, template_rendered
|
||||||
from django.test.utils import (
|
from django.test.utils import (
|
||||||
|
@ -151,6 +151,7 @@ class SimpleTestCase(unittest.TestCase):
|
||||||
# The class we'll use for the test client self.client.
|
# The class we'll use for the test client self.client.
|
||||||
# Can be overridden in derived classes.
|
# Can be overridden in derived classes.
|
||||||
client_class = Client
|
client_class = Client
|
||||||
|
async_client_class = AsyncClient
|
||||||
_overridden_settings = None
|
_overridden_settings = None
|
||||||
_modified_settings = None
|
_modified_settings = None
|
||||||
|
|
||||||
|
@ -292,6 +293,7 @@ class SimpleTestCase(unittest.TestCase):
|
||||||
* Clear the mail test outbox.
|
* Clear the mail test outbox.
|
||||||
"""
|
"""
|
||||||
self.client = self.client_class()
|
self.client = self.client_class()
|
||||||
|
self.async_client = self.async_client_class()
|
||||||
mail.outbox = []
|
mail.outbox = []
|
||||||
|
|
||||||
def _post_teardown(self):
|
def _post_teardown(self):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
@ -362,12 +363,22 @@ class TestContextDecorator:
|
||||||
raise TypeError('Can only decorate subclasses of unittest.TestCase')
|
raise TypeError('Can only decorate subclasses of unittest.TestCase')
|
||||||
|
|
||||||
def decorate_callable(self, func):
|
def decorate_callable(self, func):
|
||||||
@wraps(func)
|
if asyncio.iscoroutinefunction(func):
|
||||||
def inner(*args, **kwargs):
|
# If the inner function is an async function, we must execute async
|
||||||
with self as context:
|
# as well so that the `with` statement executes at the right time.
|
||||||
if self.kwarg_name:
|
@wraps(func)
|
||||||
kwargs[self.kwarg_name] = context
|
async def inner(*args, **kwargs):
|
||||||
return func(*args, **kwargs)
|
with self as context:
|
||||||
|
if self.kwarg_name:
|
||||||
|
kwargs[self.kwarg_name] = context
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
@wraps(func)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
with self as context:
|
||||||
|
if self.kwarg_name:
|
||||||
|
kwargs[self.kwarg_name] = context
|
||||||
|
return func(*args, **kwargs)
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
def __call__(self, decorated):
|
def __call__(self, decorated):
|
||||||
|
|
|
@ -150,3 +150,30 @@ def make_middleware_decorator(middleware_class):
|
||||||
return _wrapped_view
|
return _wrapped_view
|
||||||
return _decorator
|
return _decorator
|
||||||
return _make_decorator
|
return _make_decorator
|
||||||
|
|
||||||
|
|
||||||
|
def sync_and_async_middleware(func):
|
||||||
|
"""
|
||||||
|
Mark a middleware factory as returning a hybrid middleware supporting both
|
||||||
|
types of request.
|
||||||
|
"""
|
||||||
|
func.sync_capable = True
|
||||||
|
func.async_capable = True
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def sync_only_middleware(func):
|
||||||
|
"""
|
||||||
|
Mark a middleware factory as returning a sync middleware.
|
||||||
|
This is the default.
|
||||||
|
"""
|
||||||
|
func.sync_capable = True
|
||||||
|
func.async_capable = False
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def async_only_middleware(func):
|
||||||
|
"""Mark a middleware factory as returning an async middleware."""
|
||||||
|
func.sync_capable = False
|
||||||
|
func.async_capable = True
|
||||||
|
return func
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
|
|
||||||
class RemovedInNextVersionWarning(DeprecationWarning):
|
class RemovedInNextVersionWarning(DeprecationWarning):
|
||||||
pass
|
pass
|
||||||
|
@ -80,14 +83,31 @@ class DeprecationInstanceCheck(type):
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareMixin:
|
class MiddlewareMixin:
|
||||||
|
sync_capable = True
|
||||||
|
async_capable = True
|
||||||
|
|
||||||
# RemovedInDjango40Warning: when the deprecation ends, replace with:
|
# RemovedInDjango40Warning: when the deprecation ends, replace with:
|
||||||
# def __init__(self, get_response):
|
# def __init__(self, get_response):
|
||||||
def __init__(self, get_response=None):
|
def __init__(self, get_response=None):
|
||||||
self._get_response_none_deprecation(get_response)
|
self._get_response_none_deprecation(get_response)
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
self._async_check()
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
def _async_check(self):
|
||||||
|
"""
|
||||||
|
If get_response is a coroutine function, turns us into async mode so
|
||||||
|
a thread is not consumed during a whole request.
|
||||||
|
"""
|
||||||
|
if asyncio.iscoroutinefunction(self.get_response):
|
||||||
|
# Mark the class as async-capable, but do the actual switch
|
||||||
|
# inside __call__ to avoid swapping out dunder methods
|
||||||
|
self._is_coroutine = asyncio.coroutines._is_coroutine
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
|
# Exit out to async mode, if needed
|
||||||
|
if asyncio.iscoroutinefunction(self.get_response):
|
||||||
|
return self.__acall__(request)
|
||||||
response = None
|
response = None
|
||||||
if hasattr(self, 'process_request'):
|
if hasattr(self, 'process_request'):
|
||||||
response = self.process_request(request)
|
response = self.process_request(request)
|
||||||
|
@ -96,6 +116,19 @@ class MiddlewareMixin:
|
||||||
response = self.process_response(request, response)
|
response = self.process_response(request, response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
async def __acall__(self, request):
|
||||||
|
"""
|
||||||
|
Async version of __call__ that is swapped in when an async request
|
||||||
|
is running.
|
||||||
|
"""
|
||||||
|
response = None
|
||||||
|
if hasattr(self, 'process_request'):
|
||||||
|
response = await sync_to_async(self.process_request)(request)
|
||||||
|
response = response or await self.get_response(request)
|
||||||
|
if hasattr(self, 'process_response'):
|
||||||
|
response = await sync_to_async(self.process_response)(request, response)
|
||||||
|
return response
|
||||||
|
|
||||||
def _get_response_none_deprecation(self, get_response):
|
def _get_response_none_deprecation(self, get_response):
|
||||||
if get_response is None:
|
if get_response is None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
|
|
@ -110,8 +110,7 @@ manipulating the data of your Web application. Learn more about it below:
|
||||||
:doc:`Custom lookups <howto/custom-lookups>` |
|
:doc:`Custom lookups <howto/custom-lookups>` |
|
||||||
:doc:`Query Expressions <ref/models/expressions>` |
|
:doc:`Query Expressions <ref/models/expressions>` |
|
||||||
:doc:`Conditional Expressions <ref/models/conditional-expressions>` |
|
:doc:`Conditional Expressions <ref/models/conditional-expressions>` |
|
||||||
:doc:`Database Functions <ref/models/database-functions>` |
|
:doc:`Database Functions <ref/models/database-functions>`
|
||||||
:doc:`Asynchronous Support <topics/async>`
|
|
||||||
|
|
||||||
* **Other:**
|
* **Other:**
|
||||||
:doc:`Supported databases <ref/databases>` |
|
:doc:`Supported databases <ref/databases>` |
|
||||||
|
@ -131,7 +130,8 @@ to know about views via the links below:
|
||||||
:doc:`URLconfs <topics/http/urls>` |
|
:doc:`URLconfs <topics/http/urls>` |
|
||||||
:doc:`View functions <topics/http/views>` |
|
:doc:`View functions <topics/http/views>` |
|
||||||
:doc:`Shortcuts <topics/http/shortcuts>` |
|
:doc:`Shortcuts <topics/http/shortcuts>` |
|
||||||
:doc:`Decorators <topics/http/decorators>`
|
:doc:`Decorators <topics/http/decorators>` |
|
||||||
|
:doc:`Asynchronous Support <topics/async>`
|
||||||
|
|
||||||
* **Reference:**
|
* **Reference:**
|
||||||
:doc:`Built-in Views <ref/views>` |
|
:doc:`Built-in Views <ref/views>` |
|
||||||
|
|
|
@ -210,6 +210,31 @@ The functions defined in this module share the following properties:
|
||||||
def my_view(request):
|
def my_view(request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
.. function:: sync_only_middleware(middleware)
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
Marks a middleware as :ref:`synchronous-only <async-middleware>`. (The
|
||||||
|
default in Django, but this allows you to future-proof if the default ever
|
||||||
|
changes in a future release.)
|
||||||
|
|
||||||
|
.. function:: async_only_middleware(middleware)
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
Marks a middleware as :ref:`asynchronous-only <async-middleware>`. Django
|
||||||
|
will wrap it in an asynchronous event loop when it is called from the WSGI
|
||||||
|
request path.
|
||||||
|
|
||||||
|
.. function:: sync_and_async_middleware(middleware)
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
Marks a middleware as :ref:`sync and async compatible <async-middleware>`,
|
||||||
|
this allows to avoid converting requests. You must implement detection of
|
||||||
|
the current request type to use this decorator. See :ref:`asynchronous
|
||||||
|
middleware documentation <async-middleware>` for details.
|
||||||
|
|
||||||
``django.utils.encoding``
|
``django.utils.encoding``
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,43 @@ officially support the latest release of each series.
|
||||||
What's new in Django 3.1
|
What's new in Django 3.1
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
Asynchronous views and middleware support
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
Django now supports a fully asynchronous request path, including:
|
||||||
|
|
||||||
|
* :ref:`Asynchronous views <async-views>`
|
||||||
|
* :ref:`Asynchronous middleware <async-middleware>`
|
||||||
|
* :ref:`Asynchronous tests and test client <async-tests>`
|
||||||
|
|
||||||
|
To get started with async views, you need to declare a view using
|
||||||
|
``async def``::
|
||||||
|
|
||||||
|
async def my_view(request):
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
return HttpResponse('Hello, async world!')
|
||||||
|
|
||||||
|
All asynchronous features are supported whether you are running under WSGI or
|
||||||
|
ASGI mode. However, there will be performance penalties using async code in
|
||||||
|
WSGI mode. You can read more about the specifics in :doc:`/topics/async`
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
You are free to mix async and sync views, middleware, and tests as much as you
|
||||||
|
want. Django will ensure that you always end up with the right execution
|
||||||
|
context. We expect most projects will keep the majority of their views
|
||||||
|
synchronous, and only have a select few running in async mode - but it is
|
||||||
|
entirely your choice.
|
||||||
|
|
||||||
|
Django's ORM, cache layer, and other pieces of code that do long-running
|
||||||
|
network calls do not yet support async access. We expect to add support for
|
||||||
|
them in upcoming releases. Async views are ideal, however, if you are doing a
|
||||||
|
lot of API or HTTP calls inside your view, you can now natively do all those
|
||||||
|
HTTP calls in parallel to considerably speed up your view's execution.
|
||||||
|
|
||||||
|
Asynchronous support should be entirely backwards-compatible and we have tried
|
||||||
|
to ensure that it has no speed regressions for your existing, synchronous code.
|
||||||
|
It should have no noticeable effect on any existing Django projects.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -144,6 +144,7 @@ databrowse
|
||||||
datafile
|
datafile
|
||||||
dataset
|
dataset
|
||||||
datasets
|
datasets
|
||||||
|
datastores
|
||||||
datatype
|
datatype
|
||||||
datetimes
|
datetimes
|
||||||
Debian
|
Debian
|
||||||
|
|
|
@ -6,13 +6,106 @@ Asynchronous support
|
||||||
|
|
||||||
.. currentmodule:: asgiref.sync
|
.. currentmodule:: asgiref.sync
|
||||||
|
|
||||||
Django has developing support for asynchronous ("async") Python, but does not
|
Django has support for writing asynchronous ("async") views, along with an
|
||||||
yet support asynchronous views or middleware; they will be coming in a future
|
entirely async-enabled request stack if you are running under
|
||||||
release.
|
:doc:`ASGI </howto/deployment/asgi/index>` rather than WSGI. Async views will
|
||||||
|
still work under WSGI, but with performance penalties, and without the ability
|
||||||
|
to have efficient long-running requests.
|
||||||
|
|
||||||
There is limited support for other parts of the async ecosystem; namely, Django
|
We're still working on asynchronous support for the ORM and other parts of
|
||||||
can natively talk :doc:`ASGI </howto/deployment/asgi/index>`, and some async
|
Django; you can expect to see these in future releases. For now, you can use
|
||||||
safety support.
|
the :func:`sync_to_async` adapter to interact with normal Django, as well as
|
||||||
|
use a whole range of Python asyncio libraries natively. See below for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.1
|
||||||
|
|
||||||
|
Support for async views was added.
|
||||||
|
|
||||||
|
Async views
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
Any view can be declared async by making the callable part of it return a
|
||||||
|
coroutine - commonly, this is done using ``async def``. For a function-based
|
||||||
|
view, this means declaring the whole view using ``async def``. For a
|
||||||
|
class-based view, this means making its ``__call__()`` method an ``async def``
|
||||||
|
(not its ``__init__()`` or ``as_view()``).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Django uses ``asyncio.iscoroutinefunction`` to test if your view is
|
||||||
|
asynchronous or not. If you implement your own method of returning a
|
||||||
|
coroutine, ensure you set the ``_is_coroutine`` attribute of the view
|
||||||
|
to ``asyncio.coroutines._is_coroutine`` so this function returns ``True``.
|
||||||
|
|
||||||
|
Under a WSGI server, asynchronous views will run in their own, one-off event
|
||||||
|
loop. This means that you can do things like parallel, async HTTP calls to APIs
|
||||||
|
without any issues, but you will not get the benefits of an asynchronous
|
||||||
|
request stack.
|
||||||
|
|
||||||
|
If you want these benefits - which are mostly around the ability to service
|
||||||
|
hundreds of connections without using any Python threads (enabling slow
|
||||||
|
streaming, long-polling, and other exciting response types) - you will need to
|
||||||
|
deploy Django using :doc:`ASGI </howto/deployment/asgi/index>` instead.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
You will only get the benefits of a fully-asynchronous request stack if you
|
||||||
|
have *no synchronous middleware* loaded into your site; if there is a piece
|
||||||
|
of synchronous middleware, then Django must use a thread per request to
|
||||||
|
safely emulate a synchronous environment for it.
|
||||||
|
|
||||||
|
Middleware can be built to support :ref:`both sync and async
|
||||||
|
<async-middleware>` contexts. Some of Django's middleware is built like
|
||||||
|
this, but not all. To see what middleware Django has to adapt, you can turn
|
||||||
|
on debug logging for the ``django.request`` logger and look for log
|
||||||
|
messages about *`"Synchronous middleware ... adapted"*.
|
||||||
|
|
||||||
|
In either ASGI or WSGI mode, though, you can safely use asynchronous support to
|
||||||
|
run code in parallel rather than serially, which is especially handy when
|
||||||
|
dealing with external APIs or datastores.
|
||||||
|
|
||||||
|
If you want to call a part of Django that is still synchronous (like the ORM)
|
||||||
|
you will need to wrap it in a :func:`sync_to_async` call, like this::
|
||||||
|
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
|
results = sync_to_async(MyModel.objects.get)(pk=123)
|
||||||
|
|
||||||
|
You may find it easier to move any ORM code into its own function and call that
|
||||||
|
entire function using :func:`sync_to_async`. If you accidentally try to call
|
||||||
|
part of Django that is still synchronous-only from an async view, you will
|
||||||
|
trigger Django's :ref:`asynchronous safety protection <async-safety>` to
|
||||||
|
protect your data from corruption.
|
||||||
|
|
||||||
|
Performance
|
||||||
|
-----------
|
||||||
|
|
||||||
|
When running in a mode that does not match the view (e.g. an async view under
|
||||||
|
WSGI, or a traditional sync view under ASGI), Django must emulate the other
|
||||||
|
call style to allow your code to run. This context-switch causes a small
|
||||||
|
performance penalty of around a millisecond.
|
||||||
|
|
||||||
|
This is true of middleware as well, however. Django will attempt to minimize
|
||||||
|
the number of context-switches. If you have an ASGI server, but all your
|
||||||
|
middleware and views are synchronous, it will switch just once, before it
|
||||||
|
enters the middleware stack.
|
||||||
|
|
||||||
|
If, however, you put synchronous middleware between an ASGI server and an
|
||||||
|
asynchronous view, it will have to switch into sync mode for the middleware and
|
||||||
|
then back to asynchronous mode for the view, holding the synchronous thread
|
||||||
|
open for middleware exception propagation. This may not be noticeable, but bear
|
||||||
|
in mind that even adding a single piece of synchronous middleware can drag your
|
||||||
|
whole async project down to running with one thread per request, and the
|
||||||
|
associated performance penalties.
|
||||||
|
|
||||||
|
You should do your own performance testing to see what effect ASGI vs. WSGI has
|
||||||
|
on your code. In some cases, there may be a performance increase even for
|
||||||
|
purely-synchronous codebase under ASGI because the request-handling code is
|
||||||
|
still all running asynchronously. In general, though, you will only want to
|
||||||
|
enable ASGI mode if you have asynchronous code in your site.
|
||||||
|
|
||||||
.. _async-safety:
|
.. _async-safety:
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,10 @@ method from the handler which takes care of applying :ref:`view middleware
|
||||||
applying :ref:`template-response <template-response-middleware>` and
|
applying :ref:`template-response <template-response-middleware>` and
|
||||||
:ref:`exception <exception-middleware>` middleware.
|
:ref:`exception <exception-middleware>` middleware.
|
||||||
|
|
||||||
|
Middleware can either support only synchronous Python (the default), only
|
||||||
|
asynchronous Python, or both. See :ref:`async-middleware` for details of how to
|
||||||
|
advertise what you support, and know what kind of request you are getting.
|
||||||
|
|
||||||
Middleware can live anywhere on your Python path.
|
Middleware can live anywhere on your Python path.
|
||||||
|
|
||||||
``__init__(get_response)``
|
``__init__(get_response)``
|
||||||
|
@ -282,6 +286,81 @@ if the very next middleware in the chain raises an
|
||||||
that exception; instead it will get an :class:`~django.http.HttpResponse`
|
that exception; instead it will get an :class:`~django.http.HttpResponse`
|
||||||
object with a :attr:`~django.http.HttpResponse.status_code` of 404.
|
object with a :attr:`~django.http.HttpResponse.status_code` of 404.
|
||||||
|
|
||||||
|
.. _async-middleware:
|
||||||
|
|
||||||
|
Asynchronous support
|
||||||
|
====================
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
Middleware can support any combination of synchronous and asynchronous
|
||||||
|
requests. Django will adapt requests to fit the middleware's requirements if it
|
||||||
|
cannot support both, but at a performance penalty.
|
||||||
|
|
||||||
|
By default, Django assumes that your middleware is capable of handling only
|
||||||
|
synchronous requests. To change these assumptions, set the following attributes
|
||||||
|
on your middleware factory function or class:
|
||||||
|
|
||||||
|
* ``sync_capable`` is a boolean indicating if the middleware can handle
|
||||||
|
synchronous requests. Defaults to ``True``.
|
||||||
|
|
||||||
|
* ``async_capable`` is a boolean indicating if the middleware can handle
|
||||||
|
asynchronous requests. Defaults to ``False``.
|
||||||
|
|
||||||
|
If your middleware has both ``sync_capable = True`` and
|
||||||
|
``async_capable = True``, then Django will pass it the request in whatever form
|
||||||
|
it is currently in. You can work out what type of request you have by seeing
|
||||||
|
if the ``get_response`` object you are passed is a coroutine function or not
|
||||||
|
(using :py:func:`asyncio.iscoroutinefunction`).
|
||||||
|
|
||||||
|
The ``django.utils.decorators`` module contains
|
||||||
|
:func:`~django.utils.decorators.sync_only_middleware`,
|
||||||
|
:func:`~django.utils.decorators.async_only_middleware`, and
|
||||||
|
:func:`~django.utils.decorators.sync_and_async_middleware` decorators that
|
||||||
|
allow you to apply these flags to middleware factory functions.
|
||||||
|
|
||||||
|
The returned callable must match the sync or async nature of the
|
||||||
|
``get_response`` method. If you have an asynchronous ``get_response``, you must
|
||||||
|
return a coroutine function (``async def``).
|
||||||
|
|
||||||
|
``process_view``, ``process_template_response`` and ``process_exception``
|
||||||
|
methods, if they are provided, should also be adapted to match the sync/async
|
||||||
|
mode. However, Django will individually adapt them as required if you do not,
|
||||||
|
at an additional performance penalty.
|
||||||
|
|
||||||
|
Here's an example of how to detect and adapt your middleware if it supports
|
||||||
|
both::
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from django.utils.decorators import sync_and_async_middleware
|
||||||
|
|
||||||
|
@sync_and_async_middleware
|
||||||
|
def simple_middleware(get_response):
|
||||||
|
# One-time configuration and initialization goes here.
|
||||||
|
if asyncio.iscoroutinefunction(get_response):
|
||||||
|
async def middleware(request):
|
||||||
|
# Do something here!
|
||||||
|
response = await get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
else:
|
||||||
|
def middleware(request):
|
||||||
|
# Do something here!
|
||||||
|
response = get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
return middleware
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you declare a hybrid middleware that supports both synchronous and
|
||||||
|
asynchronous calls, the kind of call you get may not match the underlying
|
||||||
|
view. Django will optimize the middleware call stack to have as few
|
||||||
|
sync/async transitions as possible.
|
||||||
|
|
||||||
|
Thus, even if you are wrapping an async view, you may be called in sync
|
||||||
|
mode if there is other, synchronous middleware between you and the view.
|
||||||
|
|
||||||
.. _upgrading-middleware:
|
.. _upgrading-middleware:
|
||||||
|
|
||||||
Upgrading pre-Django 1.10-style middleware
|
Upgrading pre-Django 1.10-style middleware
|
||||||
|
@ -292,8 +371,8 @@ Upgrading pre-Django 1.10-style middleware
|
||||||
|
|
||||||
Django provides ``django.utils.deprecation.MiddlewareMixin`` to ease creating
|
Django provides ``django.utils.deprecation.MiddlewareMixin`` to ease creating
|
||||||
middleware classes that are compatible with both :setting:`MIDDLEWARE` and the
|
middleware classes that are compatible with both :setting:`MIDDLEWARE` and the
|
||||||
old ``MIDDLEWARE_CLASSES``. All middleware classes included with Django
|
old ``MIDDLEWARE_CLASSES``, and support synchronous and asynchronous requests.
|
||||||
are compatible with both settings.
|
All middleware classes included with Django are compatible with both settings.
|
||||||
|
|
||||||
The mixin provides an ``__init__()`` method that requires a ``get_response``
|
The mixin provides an ``__init__()`` method that requires a ``get_response``
|
||||||
argument and stores it in ``self.get_response``.
|
argument and stores it in ``self.get_response``.
|
||||||
|
@ -345,3 +424,7 @@ These are the behavioral differences between using :setting:`MIDDLEWARE` and
|
||||||
HTTP response, and then the next middleware in line will see that
|
HTTP response, and then the next middleware in line will see that
|
||||||
response. Middleware are never skipped due to a middleware raising an
|
response. Middleware are never skipped due to a middleware raising an
|
||||||
exception.
|
exception.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.1
|
||||||
|
|
||||||
|
Support for asynchronous requests was added to the ``MiddlewareMixin``.
|
||||||
|
|
|
@ -202,3 +202,28 @@ in a test view. For example::
|
||||||
response = self.client.get('/403/')
|
response = self.client.get('/403/')
|
||||||
# Make assertions on the response here. For example:
|
# Make assertions on the response here. For example:
|
||||||
self.assertContains(response, 'Error handler content', status_code=403)
|
self.assertContains(response, 'Error handler content', status_code=403)
|
||||||
|
|
||||||
|
.. _async-views:
|
||||||
|
|
||||||
|
Asynchronous views
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
As well as being synchronous functions, views can also be asynchronous
|
||||||
|
functions (``async def``). Django will automatically detect these and run them
|
||||||
|
in an asynchronous context. You will need to be using an asynchronous (ASGI)
|
||||||
|
server to get the full power of them, however.
|
||||||
|
|
||||||
|
Here's an example of an asynchronous view::
|
||||||
|
|
||||||
|
from django.http import HttpResponse
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
async def current_datetime(request):
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
html = '<html><body>It is now %s.</body></html>' % now
|
||||||
|
return HttpResponse(html)
|
||||||
|
|
||||||
|
You can read more about Django's asynchronous support, and how to best use
|
||||||
|
asynchronous views, in :doc:`/topics/async`.
|
||||||
|
|
|
@ -67,6 +67,17 @@ The following is a unit test using the request factory::
|
||||||
response = MyView.as_view()(request)
|
response = MyView.as_view()(request)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
AsyncRequestFactory
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
``RequestFactory`` creates WSGI-like requests. If you want to create ASGI-like
|
||||||
|
requests, including having a correct ASGI ``scope``, you can instead use
|
||||||
|
``django.test.AsyncRequestFactory``.
|
||||||
|
|
||||||
|
This class is directly API-compatible with ``RequestFactory``, with the only
|
||||||
|
difference being that it returns ``ASGIRequest`` instances rather than
|
||||||
|
``WSGIRequest`` instances. All of its methods are still synchronous callables.
|
||||||
|
|
||||||
Testing class-based views
|
Testing class-based views
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -1755,6 +1755,62 @@ You can also exclude tests by tag. To run core tests if they are not slow:
|
||||||
test has two tags and you select one of them and exclude the other, the test
|
test has two tags and you select one of them and exclude the other, the test
|
||||||
won't be run.
|
won't be run.
|
||||||
|
|
||||||
|
.. _async-tests:
|
||||||
|
|
||||||
|
Testing asynchronous code
|
||||||
|
=========================
|
||||||
|
|
||||||
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
If you merely want to test the output of your asynchronous views, the standard
|
||||||
|
test client will run them inside their own asynchronous loop without any extra
|
||||||
|
work needed on your part.
|
||||||
|
|
||||||
|
However, if you want to write fully-asynchronous tests for a Django project,
|
||||||
|
you will need to take several things into account.
|
||||||
|
|
||||||
|
Firstly, your tests must be ``async def`` methods on the test class (in order
|
||||||
|
to give them an asynchronous context). Django will automatically detect
|
||||||
|
any ``async def`` tests and wrap them so they run in their own event loop.
|
||||||
|
|
||||||
|
If you are testing from an asynchronous function, you must also use the
|
||||||
|
asynchronous test client. This is available as ``django.test.AsyncClient``,
|
||||||
|
or as ``self.async_client`` on any test.
|
||||||
|
|
||||||
|
With the exception of the ``follow`` parameter, which is not supported,
|
||||||
|
``AsyncClient`` has the same methods and signatures as the synchronous (normal)
|
||||||
|
test client, but any method that makes a request must be awaited::
|
||||||
|
|
||||||
|
async def test_my_thing(self):
|
||||||
|
response = await self.async_client.get('/some-url/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
The asynchronous client can also call synchronous views; it runs through
|
||||||
|
Django's :doc:`asynchronous request path </topics/async>`, which supports both.
|
||||||
|
Any view called through the ``AsyncClient`` will get an ``ASGIRequest`` object
|
||||||
|
for its ``request`` rather than the ``WSGIRequest`` that the normal client
|
||||||
|
creates.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
If you are using test decorators, they must be async-compatible to ensure
|
||||||
|
they work correctly. Django's built-in decorators will behave correctly, but
|
||||||
|
third-party ones may appear to not execute (they will "wrap" the wrong part
|
||||||
|
of the execution flow and not your test).
|
||||||
|
|
||||||
|
If you need to use these decorators, then you should decorate your test
|
||||||
|
methods with :func:`~asgiref.sync.async_to_sync` *inside* of them instead::
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
class MyTests(TestCase):
|
||||||
|
|
||||||
|
@mock.patch(...)
|
||||||
|
@async_to_sync
|
||||||
|
def test_my_thing(self):
|
||||||
|
...
|
||||||
|
|
||||||
.. _topics-testing-email:
|
.. _topics-testing-email:
|
||||||
|
|
||||||
Email services
|
Email services
|
||||||
|
|
|
@ -7,7 +7,7 @@ from asgiref.testing import ApplicationCommunicator
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
from django.core.signals import request_started
|
from django.core.signals import request_started
|
||||||
from django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import AsyncRequestFactory, SimpleTestCase, override_settings
|
||||||
|
|
||||||
from .urls import test_filename
|
from .urls import test_filename
|
||||||
|
|
||||||
|
@ -15,21 +15,11 @@ from .urls import test_filename
|
||||||
@skipIf(sys.platform == 'win32' and (3, 8, 0) < sys.version_info < (3, 8, 1), 'https://bugs.python.org/issue38563')
|
@skipIf(sys.platform == 'win32' and (3, 8, 0) < sys.version_info < (3, 8, 1), 'https://bugs.python.org/issue38563')
|
||||||
@override_settings(ROOT_URLCONF='asgi.urls')
|
@override_settings(ROOT_URLCONF='asgi.urls')
|
||||||
class ASGITest(SimpleTestCase):
|
class ASGITest(SimpleTestCase):
|
||||||
|
async_request_factory = AsyncRequestFactory()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
request_started.disconnect(close_old_connections)
|
request_started.disconnect(close_old_connections)
|
||||||
|
|
||||||
def _get_scope(self, **kwargs):
|
|
||||||
return {
|
|
||||||
'type': 'http',
|
|
||||||
'asgi': {'version': '3.0', 'spec_version': '2.1'},
|
|
||||||
'http_version': '1.1',
|
|
||||||
'method': 'GET',
|
|
||||||
'query_string': b'',
|
|
||||||
'server': ('testserver', 80),
|
|
||||||
**kwargs,
|
|
||||||
}
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
request_started.connect(close_old_connections)
|
request_started.connect(close_old_connections)
|
||||||
|
|
||||||
|
@ -39,7 +29,8 @@ class ASGITest(SimpleTestCase):
|
||||||
"""
|
"""
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
# Construct HTTP request.
|
# Construct HTTP request.
|
||||||
communicator = ApplicationCommunicator(application, self._get_scope(path='/'))
|
scope = self.async_request_factory._base_scope(path='/')
|
||||||
|
communicator = ApplicationCommunicator(application, scope)
|
||||||
await communicator.send_input({'type': 'http.request'})
|
await communicator.send_input({'type': 'http.request'})
|
||||||
# Read the response.
|
# Read the response.
|
||||||
response_start = await communicator.receive_output()
|
response_start = await communicator.receive_output()
|
||||||
|
@ -62,7 +53,8 @@ class ASGITest(SimpleTestCase):
|
||||||
"""
|
"""
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
# Construct HTTP request.
|
# Construct HTTP request.
|
||||||
communicator = ApplicationCommunicator(application, self._get_scope(path='/file/'))
|
scope = self.async_request_factory._base_scope(path='/file/')
|
||||||
|
communicator = ApplicationCommunicator(application, scope)
|
||||||
await communicator.send_input({'type': 'http.request'})
|
await communicator.send_input({'type': 'http.request'})
|
||||||
# Get the file content.
|
# Get the file content.
|
||||||
with open(test_filename, 'rb') as test_file:
|
with open(test_filename, 'rb') as test_file:
|
||||||
|
@ -82,12 +74,14 @@ class ASGITest(SimpleTestCase):
|
||||||
response_body = await communicator.receive_output()
|
response_body = await communicator.receive_output()
|
||||||
self.assertEqual(response_body['type'], 'http.response.body')
|
self.assertEqual(response_body['type'], 'http.response.body')
|
||||||
self.assertEqual(response_body['body'], test_file_contents)
|
self.assertEqual(response_body['body'], test_file_contents)
|
||||||
|
# Allow response.close() to finish.
|
||||||
|
await communicator.wait()
|
||||||
|
|
||||||
async def test_headers(self):
|
async def test_headers(self):
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
communicator = ApplicationCommunicator(
|
communicator = ApplicationCommunicator(
|
||||||
application,
|
application,
|
||||||
self._get_scope(
|
self.async_request_factory._base_scope(
|
||||||
path='/meta/',
|
path='/meta/',
|
||||||
headers=[
|
headers=[
|
||||||
[b'content-type', b'text/plain; charset=utf-8'],
|
[b'content-type', b'text/plain; charset=utf-8'],
|
||||||
|
@ -116,10 +110,11 @@ class ASGITest(SimpleTestCase):
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
for query_string in (b'name=Andrew', 'name=Andrew'):
|
for query_string in (b'name=Andrew', 'name=Andrew'):
|
||||||
with self.subTest(query_string=query_string):
|
with self.subTest(query_string=query_string):
|
||||||
communicator = ApplicationCommunicator(
|
scope = self.async_request_factory._base_scope(
|
||||||
application,
|
path='/',
|
||||||
self._get_scope(path='/', query_string=query_string),
|
query_string=query_string,
|
||||||
)
|
)
|
||||||
|
communicator = ApplicationCommunicator(application, scope)
|
||||||
await communicator.send_input({'type': 'http.request'})
|
await communicator.send_input({'type': 'http.request'})
|
||||||
response_start = await communicator.receive_output()
|
response_start = await communicator.receive_output()
|
||||||
self.assertEqual(response_start['type'], 'http.response.start')
|
self.assertEqual(response_start['type'], 'http.response.start')
|
||||||
|
@ -130,17 +125,16 @@ class ASGITest(SimpleTestCase):
|
||||||
|
|
||||||
async def test_disconnect(self):
|
async def test_disconnect(self):
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
communicator = ApplicationCommunicator(application, self._get_scope(path='/'))
|
scope = self.async_request_factory._base_scope(path='/')
|
||||||
|
communicator = ApplicationCommunicator(application, scope)
|
||||||
await communicator.send_input({'type': 'http.disconnect'})
|
await communicator.send_input({'type': 'http.disconnect'})
|
||||||
with self.assertRaises(asyncio.TimeoutError):
|
with self.assertRaises(asyncio.TimeoutError):
|
||||||
await communicator.receive_output()
|
await communicator.receive_output()
|
||||||
|
|
||||||
async def test_wrong_connection_type(self):
|
async def test_wrong_connection_type(self):
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
communicator = ApplicationCommunicator(
|
scope = self.async_request_factory._base_scope(path='/', type='other')
|
||||||
application,
|
communicator = ApplicationCommunicator(application, scope)
|
||||||
self._get_scope(path='/', type='other'),
|
|
||||||
)
|
|
||||||
await communicator.send_input({'type': 'http.request'})
|
await communicator.send_input({'type': 'http.request'})
|
||||||
msg = 'Django can only handle ASGI/HTTP connections, not other.'
|
msg = 'Django can only handle ASGI/HTTP connections, not other.'
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
@ -148,10 +142,8 @@ class ASGITest(SimpleTestCase):
|
||||||
|
|
||||||
async def test_non_unicode_query_string(self):
|
async def test_non_unicode_query_string(self):
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
communicator = ApplicationCommunicator(
|
scope = self.async_request_factory._base_scope(path='/', query_string=b'\xff')
|
||||||
application,
|
communicator = ApplicationCommunicator(application, scope)
|
||||||
self._get_scope(path='/', query_string=b'\xff'),
|
|
||||||
)
|
|
||||||
await communicator.send_input({'type': 'http.request'})
|
await communicator.send_input({'type': 'http.request'})
|
||||||
response_start = await communicator.receive_output()
|
response_start = await communicator.receive_output()
|
||||||
self.assertEqual(response_start['type'], 'http.response.start')
|
self.assertEqual(response_start['type'], 'http.response.start')
|
||||||
|
|
|
@ -106,6 +106,16 @@ class TransactionsPerRequestTests(TransactionTestCase):
|
||||||
connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
|
connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
|
||||||
self.assertContains(response, 'True')
|
self.assertContains(response, 'True')
|
||||||
|
|
||||||
|
async def test_auto_transaction_async_view(self):
|
||||||
|
old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
|
||||||
|
try:
|
||||||
|
connection.settings_dict['ATOMIC_REQUESTS'] = True
|
||||||
|
msg = 'You cannot use ATOMIC_REQUESTS with async views.'
|
||||||
|
with self.assertRaisesMessage(RuntimeError, msg):
|
||||||
|
await self.async_client.get('/async_regular/')
|
||||||
|
finally:
|
||||||
|
connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests
|
||||||
|
|
||||||
def test_no_auto_transaction(self):
|
def test_no_auto_transaction(self):
|
||||||
old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
|
old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS']
|
||||||
try:
|
try:
|
||||||
|
@ -157,6 +167,11 @@ def empty_middleware(get_response):
|
||||||
class HandlerRequestTests(SimpleTestCase):
|
class HandlerRequestTests(SimpleTestCase):
|
||||||
request_factory = RequestFactory()
|
request_factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_async_view(self):
|
||||||
|
"""Calling an async view down the normal synchronous path."""
|
||||||
|
response = self.client.get('/async_regular/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_suspiciousop_in_view_returns_400(self):
|
def test_suspiciousop_in_view_returns_400(self):
|
||||||
response = self.client.get('/suspicious/')
|
response = self.client.get('/suspicious/')
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
@ -224,3 +239,39 @@ class ScriptNameTests(SimpleTestCase):
|
||||||
'PATH_INFO': '/milestones/accounts/login/help',
|
'PATH_INFO': '/milestones/accounts/login/help',
|
||||||
})
|
})
|
||||||
self.assertEqual(script_name, '/mst')
|
self.assertEqual(script_name, '/mst')
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ROOT_URLCONF='handlers.urls')
|
||||||
|
class AsyncHandlerRequestTests(SimpleTestCase):
|
||||||
|
"""Async variants of the normal handler request tests."""
|
||||||
|
|
||||||
|
async def test_sync_view(self):
|
||||||
|
"""Calling a sync view down the asynchronous path."""
|
||||||
|
response = await self.async_client.get('/regular/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
async def test_async_view(self):
|
||||||
|
"""Calling an async view down the asynchronous path."""
|
||||||
|
response = await self.async_client.get('/async_regular/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
async def test_suspiciousop_in_view_returns_400(self):
|
||||||
|
response = await self.async_client.get('/suspicious/')
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
async def test_no_response(self):
|
||||||
|
msg = (
|
||||||
|
"The view handlers.views.no_response didn't return an "
|
||||||
|
"HttpResponse object. It returned None instead."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
await self.async_client.get('/no_response_fbv/')
|
||||||
|
|
||||||
|
async def test_unawaited_response(self):
|
||||||
|
msg = (
|
||||||
|
"The view handlers.views.async_unawaited didn't return an "
|
||||||
|
"HttpResponse object. It returned an unawaited coroutine instead. "
|
||||||
|
"You may need to add an 'await' into your view."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
await self.async_client.get('/unawaited/')
|
||||||
|
|
|
@ -4,6 +4,7 @@ from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('regular/', views.regular),
|
path('regular/', views.regular),
|
||||||
|
path('async_regular/', views.async_regular),
|
||||||
path('no_response_fbv/', views.no_response),
|
path('no_response_fbv/', views.no_response),
|
||||||
path('no_response_cbv/', views.NoResponse()),
|
path('no_response_cbv/', views.NoResponse()),
|
||||||
path('streaming/', views.streaming),
|
path('streaming/', views.streaming),
|
||||||
|
@ -12,4 +13,5 @@ urlpatterns = [
|
||||||
path('suspicious/', views.suspicious),
|
path('suspicious/', views.suspicious),
|
||||||
path('malformed_post/', views.malformed_post),
|
path('malformed_post/', views.malformed_post),
|
||||||
path('httpstatus_enum/', views.httpstatus_enum),
|
path('httpstatus_enum/', views.httpstatus_enum),
|
||||||
|
path('unawaited/', views.async_unawaited),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
@ -44,3 +45,12 @@ def malformed_post(request):
|
||||||
|
|
||||||
def httpstatus_enum(request):
|
def httpstatus_enum(request):
|
||||||
return HttpResponse(status=HTTPStatus.OK)
|
return HttpResponse(status=HTTPStatus.OK)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_regular(request):
|
||||||
|
return HttpResponse(b'regular content')
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unawaited(request):
|
||||||
|
"""Return an unawaited coroutine (common error for async views)."""
|
||||||
|
return asyncio.sleep(0)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.template import engines
|
from django.template import engines
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.decorators import (
|
||||||
|
async_only_middleware, sync_and_async_middleware, sync_only_middleware,
|
||||||
|
)
|
||||||
|
|
||||||
log = []
|
log = []
|
||||||
|
|
||||||
|
@ -18,6 +21,12 @@ class ProcessExceptionMiddleware(BaseMiddleware):
|
||||||
return HttpResponse('Exception caught')
|
return HttpResponse('Exception caught')
|
||||||
|
|
||||||
|
|
||||||
|
@async_only_middleware
|
||||||
|
class AsyncProcessExceptionMiddleware(BaseMiddleware):
|
||||||
|
async def process_exception(self, request, exception):
|
||||||
|
return HttpResponse('Exception caught')
|
||||||
|
|
||||||
|
|
||||||
class ProcessExceptionLogMiddleware(BaseMiddleware):
|
class ProcessExceptionLogMiddleware(BaseMiddleware):
|
||||||
def process_exception(self, request, exception):
|
def process_exception(self, request, exception):
|
||||||
log.append('process-exception')
|
log.append('process-exception')
|
||||||
|
@ -33,6 +42,12 @@ class ProcessViewMiddleware(BaseMiddleware):
|
||||||
return HttpResponse('Processed view %s' % view_func.__name__)
|
return HttpResponse('Processed view %s' % view_func.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@async_only_middleware
|
||||||
|
class AsyncProcessViewMiddleware(BaseMiddleware):
|
||||||
|
async def process_view(self, request, view_func, view_args, view_kwargs):
|
||||||
|
return HttpResponse('Processed view %s' % view_func.__name__)
|
||||||
|
|
||||||
|
|
||||||
class ProcessViewNoneMiddleware(BaseMiddleware):
|
class ProcessViewNoneMiddleware(BaseMiddleware):
|
||||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||||
log.append('processed view %s' % view_func.__name__)
|
log.append('processed view %s' % view_func.__name__)
|
||||||
|
@ -51,6 +66,13 @@ class TemplateResponseMiddleware(BaseMiddleware):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@async_only_middleware
|
||||||
|
class AsyncTemplateResponseMiddleware(BaseMiddleware):
|
||||||
|
async def process_template_response(self, request, response):
|
||||||
|
response.context_data['mw'].append(self.__class__.__name__)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class LogMiddleware(BaseMiddleware):
|
class LogMiddleware(BaseMiddleware):
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
|
@ -63,6 +85,48 @@ class NoTemplateResponseMiddleware(BaseMiddleware):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@async_only_middleware
|
||||||
|
class AsyncNoTemplateResponseMiddleware(BaseMiddleware):
|
||||||
|
async def process_template_response(self, request, response):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class NotFoundMiddleware(BaseMiddleware):
|
class NotFoundMiddleware(BaseMiddleware):
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
raise Http404('not found')
|
raise Http404('not found')
|
||||||
|
|
||||||
|
|
||||||
|
class TeapotMiddleware(BaseMiddleware):
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
response.status_code = 418
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@async_only_middleware
|
||||||
|
def async_teapot_middleware(get_response):
|
||||||
|
async def middleware(request):
|
||||||
|
response = await get_response(request)
|
||||||
|
response.status_code = 418
|
||||||
|
return response
|
||||||
|
|
||||||
|
return middleware
|
||||||
|
|
||||||
|
|
||||||
|
@sync_and_async_middleware
|
||||||
|
class SyncAndAsyncMiddleware(BaseMiddleware):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@sync_only_middleware
|
||||||
|
class DecoratedTeapotMiddleware(TeapotMiddleware):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotSyncOrAsyncMiddleware(BaseMiddleware):
|
||||||
|
"""Middleware that is deliberately neither sync or async."""
|
||||||
|
sync_capable = False
|
||||||
|
async_capable = False
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
return self.get_response(request)
|
||||||
|
|
|
@ -180,3 +180,162 @@ class MiddlewareNotUsedTests(SimpleTestCase):
|
||||||
with self.assertRaisesMessage(AssertionError, 'no logs'):
|
with self.assertRaisesMessage(AssertionError, 'no logs'):
|
||||||
with self.assertLogs('django.request', 'DEBUG'):
|
with self.assertLogs('django.request', 'DEBUG'):
|
||||||
self.client.get('/middleware_exceptions/view/')
|
self.client.get('/middleware_exceptions/view/')
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
DEBUG=True,
|
||||||
|
ROOT_URLCONF='middleware_exceptions.urls',
|
||||||
|
)
|
||||||
|
class MiddlewareSyncAsyncTests(SimpleTestCase):
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.TeapotMiddleware',
|
||||||
|
])
|
||||||
|
def test_sync_teapot_middleware(self):
|
||||||
|
response = self.client.get('/middleware_exceptions/view/')
|
||||||
|
self.assertEqual(response.status_code, 418)
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.DecoratedTeapotMiddleware',
|
||||||
|
])
|
||||||
|
def test_sync_decorated_teapot_middleware(self):
|
||||||
|
response = self.client.get('/middleware_exceptions/view/')
|
||||||
|
self.assertEqual(response.status_code, 418)
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.async_teapot_middleware',
|
||||||
|
])
|
||||||
|
def test_async_teapot_middleware(self):
|
||||||
|
with self.assertLogs('django.request', 'DEBUG') as cm:
|
||||||
|
response = self.client.get('/middleware_exceptions/view/')
|
||||||
|
self.assertEqual(response.status_code, 418)
|
||||||
|
self.assertEqual(
|
||||||
|
cm.records[0].getMessage(),
|
||||||
|
"Synchronous middleware "
|
||||||
|
"middleware_exceptions.middleware.async_teapot_middleware "
|
||||||
|
"adapted.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.NotSyncOrAsyncMiddleware',
|
||||||
|
])
|
||||||
|
def test_not_sync_or_async_middleware(self):
|
||||||
|
msg = (
|
||||||
|
'Middleware '
|
||||||
|
'middleware_exceptions.middleware.NotSyncOrAsyncMiddleware must '
|
||||||
|
'have at least one of sync_capable/async_capable set to True.'
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(RuntimeError, msg):
|
||||||
|
self.client.get('/middleware_exceptions/view/')
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.TeapotMiddleware',
|
||||||
|
])
|
||||||
|
async def test_sync_teapot_middleware_async(self):
|
||||||
|
with self.assertLogs('django.request', 'DEBUG') as cm:
|
||||||
|
response = await self.async_client.get('/middleware_exceptions/view/')
|
||||||
|
self.assertEqual(response.status_code, 418)
|
||||||
|
self.assertEqual(
|
||||||
|
cm.records[0].getMessage(),
|
||||||
|
"Asynchronous middleware "
|
||||||
|
"middleware_exceptions.middleware.TeapotMiddleware adapted.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.async_teapot_middleware',
|
||||||
|
])
|
||||||
|
async def test_async_teapot_middleware_async(self):
|
||||||
|
with self.assertLogs('django.request', 'WARNING') as cm:
|
||||||
|
response = await self.async_client.get('/middleware_exceptions/view/')
|
||||||
|
self.assertEqual(response.status_code, 418)
|
||||||
|
self.assertEqual(
|
||||||
|
cm.records[0].getMessage(),
|
||||||
|
'Unknown Status Code: /middleware_exceptions/view/',
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
DEBUG=False,
|
||||||
|
MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.AsyncNoTemplateResponseMiddleware',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_async_process_template_response_returns_none_with_sync_client(self):
|
||||||
|
msg = (
|
||||||
|
"AsyncNoTemplateResponseMiddleware.process_template_response "
|
||||||
|
"didn't return an HttpResponse object."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
self.client.get('/middleware_exceptions/template_response/')
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.SyncAndAsyncMiddleware',
|
||||||
|
])
|
||||||
|
async def test_async_and_sync_middleware_async_call(self):
|
||||||
|
response = await self.async_client.get('/middleware_exceptions/view/')
|
||||||
|
self.assertEqual(response.content, b'OK')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.SyncAndAsyncMiddleware',
|
||||||
|
])
|
||||||
|
def test_async_and_sync_middleware_sync_call(self):
|
||||||
|
response = self.client.get('/middleware_exceptions/view/')
|
||||||
|
self.assertEqual(response.content, b'OK')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ROOT_URLCONF='middleware_exceptions.urls')
|
||||||
|
class AsyncMiddlewareTests(SimpleTestCase):
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.AsyncTemplateResponseMiddleware',
|
||||||
|
])
|
||||||
|
async def test_process_template_response(self):
|
||||||
|
response = await self.async_client.get(
|
||||||
|
'/middleware_exceptions/template_response/'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.content,
|
||||||
|
b'template_response OK\nAsyncTemplateResponseMiddleware',
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.AsyncNoTemplateResponseMiddleware',
|
||||||
|
])
|
||||||
|
async def test_process_template_response_returns_none(self):
|
||||||
|
msg = (
|
||||||
|
"AsyncNoTemplateResponseMiddleware.process_template_response "
|
||||||
|
"didn't return an HttpResponse object. It returned None instead."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
await self.async_client.get('/middleware_exceptions/template_response/')
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.AsyncProcessExceptionMiddleware',
|
||||||
|
])
|
||||||
|
async def test_exception_in_render_passed_to_process_exception(self):
|
||||||
|
response = await self.async_client.get(
|
||||||
|
'/middleware_exceptions/exception_in_render/'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.content, b'Exception caught')
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.AsyncProcessExceptionMiddleware',
|
||||||
|
])
|
||||||
|
async def test_exception_in_async_render_passed_to_process_exception(self):
|
||||||
|
response = await self.async_client.get(
|
||||||
|
'/middleware_exceptions/async_exception_in_render/'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.content, b'Exception caught')
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.AsyncProcessExceptionMiddleware',
|
||||||
|
])
|
||||||
|
async def test_view_exception_handled_by_process_exception(self):
|
||||||
|
response = await self.async_client.get('/middleware_exceptions/error/')
|
||||||
|
self.assertEqual(response.content, b'Exception caught')
|
||||||
|
|
||||||
|
@override_settings(MIDDLEWARE=[
|
||||||
|
'middleware_exceptions.middleware.AsyncProcessViewMiddleware',
|
||||||
|
])
|
||||||
|
async def test_process_view_return_response(self):
|
||||||
|
response = await self.async_client.get('/middleware_exceptions/view/')
|
||||||
|
self.assertEqual(response.content, b'Processed view normal_view')
|
||||||
|
|
|
@ -8,4 +8,9 @@ urlpatterns = [
|
||||||
path('middleware_exceptions/permission_denied/', views.permission_denied),
|
path('middleware_exceptions/permission_denied/', views.permission_denied),
|
||||||
path('middleware_exceptions/exception_in_render/', views.exception_in_render),
|
path('middleware_exceptions/exception_in_render/', views.exception_in_render),
|
||||||
path('middleware_exceptions/template_response/', views.template_response),
|
path('middleware_exceptions/template_response/', views.template_response),
|
||||||
|
# Async views.
|
||||||
|
path(
|
||||||
|
'middleware_exceptions/async_exception_in_render/',
|
||||||
|
views.async_exception_in_render,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -27,3 +27,11 @@ def exception_in_render(request):
|
||||||
raise Exception('Exception in HttpResponse.render()')
|
raise Exception('Exception in HttpResponse.render()')
|
||||||
|
|
||||||
return CustomHttpResponse('Error')
|
return CustomHttpResponse('Error')
|
||||||
|
|
||||||
|
|
||||||
|
async def async_exception_in_render(request):
|
||||||
|
class CustomHttpResponse(HttpResponse):
|
||||||
|
async def render(self):
|
||||||
|
raise Exception('Exception in HttpResponse.render()')
|
||||||
|
|
||||||
|
return CustomHttpResponse('Error')
|
||||||
|
|
|
@ -25,9 +25,10 @@ from unittest import mock
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, HttpResponseNotAllowed
|
||||||
from django.test import (
|
from django.test import (
|
||||||
Client, RequestFactory, SimpleTestCase, TestCase, override_settings,
|
AsyncRequestFactory, Client, RequestFactory, SimpleTestCase, TestCase,
|
||||||
|
override_settings,
|
||||||
)
|
)
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
|
@ -918,3 +919,57 @@ class RequestFactoryTest(SimpleTestCase):
|
||||||
protocol = request.META["SERVER_PROTOCOL"]
|
protocol = request.META["SERVER_PROTOCOL"]
|
||||||
echoed_request_line = "TRACE {} {}".format(url_path, protocol)
|
echoed_request_line = "TRACE {} {}".format(url_path, protocol)
|
||||||
self.assertContains(response, echoed_request_line)
|
self.assertContains(response, echoed_request_line)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ROOT_URLCONF='test_client.urls')
|
||||||
|
class AsyncClientTest(TestCase):
|
||||||
|
async def test_response_resolver_match(self):
|
||||||
|
response = await self.async_client.get('/async_get_view/')
|
||||||
|
self.assertTrue(hasattr(response, 'resolver_match'))
|
||||||
|
self.assertEqual(response.resolver_match.url_name, 'async_get_view')
|
||||||
|
|
||||||
|
async def test_follow_parameter_not_implemented(self):
|
||||||
|
msg = 'AsyncClient request methods do not accept the follow parameter.'
|
||||||
|
tests = (
|
||||||
|
'get',
|
||||||
|
'post',
|
||||||
|
'put',
|
||||||
|
'patch',
|
||||||
|
'delete',
|
||||||
|
'head',
|
||||||
|
'options',
|
||||||
|
'trace',
|
||||||
|
)
|
||||||
|
for method_name in tests:
|
||||||
|
with self.subTest(method=method_name):
|
||||||
|
method = getattr(self.async_client, method_name)
|
||||||
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
||||||
|
await method('/redirect_view/', follow=True)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ROOT_URLCONF='test_client.urls')
|
||||||
|
class AsyncRequestFactoryTest(SimpleTestCase):
|
||||||
|
request_factory = AsyncRequestFactory()
|
||||||
|
|
||||||
|
async def test_request_factory(self):
|
||||||
|
tests = (
|
||||||
|
'get',
|
||||||
|
'post',
|
||||||
|
'put',
|
||||||
|
'patch',
|
||||||
|
'delete',
|
||||||
|
'head',
|
||||||
|
'options',
|
||||||
|
'trace',
|
||||||
|
)
|
||||||
|
for method_name in tests:
|
||||||
|
with self.subTest(method=method_name):
|
||||||
|
async def async_generic_view(request):
|
||||||
|
if request.method.lower() != method_name:
|
||||||
|
return HttpResponseNotAllowed(method_name)
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
|
method = getattr(self.request_factory, method_name)
|
||||||
|
request = method('/somewhere/')
|
||||||
|
response = await async_generic_view(request)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -44,4 +44,6 @@ urlpatterns = [
|
||||||
path('accounts/no_trailing_slash', RedirectView.as_view(url='login/')),
|
path('accounts/no_trailing_slash', RedirectView.as_view(url='login/')),
|
||||||
path('accounts/login/', auth_views.LoginView.as_view(template_name='login.html')),
|
path('accounts/login/', auth_views.LoginView.as_view(template_name='login.html')),
|
||||||
path('accounts/logout/', auth_views.LogoutView.as_view()),
|
path('accounts/logout/', auth_views.LogoutView.as_view()),
|
||||||
|
# Async views.
|
||||||
|
path('async_get_view/', views.async_get_view, name='async_get_view'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -25,6 +25,10 @@ def get_view(request):
|
||||||
return HttpResponse(t.render(c))
|
return HttpResponse(t.render(c))
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_view(request):
|
||||||
|
return HttpResponse(b'GET content.')
|
||||||
|
|
||||||
|
|
||||||
def trace_view(request):
|
def trace_view(request):
|
||||||
"""
|
"""
|
||||||
A simple view that expects a TRACE request and echoes its status line.
|
A simple view that expects a TRACE request and echoes its status line.
|
||||||
|
|
Loading…
Reference in New Issue