Fixed #30451 -- Added ASGI handler and coroutine-safety.
This adds an ASGI handler, asgi.py file for the default project layout, a few async utilities and adds async-safety to many parts of Django.
This commit is contained in:
parent
cce47ff65a
commit
a415ce70be
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for {{ project_name }} project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
|
@ -4,25 +4,20 @@ from urllib.request import url2pathname
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles import utils
|
from django.contrib.staticfiles import utils
|
||||||
from django.contrib.staticfiles.views import serve
|
from django.contrib.staticfiles.views import serve
|
||||||
|
from django.core.handlers.asgi import ASGIHandler
|
||||||
from django.core.handlers.exception import response_for_exception
|
from django.core.handlers.exception import response_for_exception
|
||||||
from django.core.handlers.wsgi import WSGIHandler, get_path_info
|
from django.core.handlers.wsgi import WSGIHandler, get_path_info
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
||||||
|
|
||||||
class StaticFilesHandler(WSGIHandler):
|
class StaticFilesHandlerMixin:
|
||||||
"""
|
"""
|
||||||
WSGI middleware that intercepts calls to the static files directory, as
|
Common methods used by WSGI and ASGI handlers.
|
||||||
defined by the STATIC_URL setting, and serves those files.
|
|
||||||
"""
|
"""
|
||||||
# May be used to differentiate between handler types (e.g. in a
|
# May be used to differentiate between handler types (e.g. in a
|
||||||
# request_finished signal)
|
# request_finished signal)
|
||||||
handles_files = True
|
handles_files = True
|
||||||
|
|
||||||
def __init__(self, application):
|
|
||||||
self.application = application
|
|
||||||
self.base_url = urlparse(self.get_base_url())
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def load_middleware(self):
|
def load_middleware(self):
|
||||||
# Middleware are already loaded for self.application; no need to reload
|
# Middleware are already loaded for self.application; no need to reload
|
||||||
# them for self.
|
# them for self.
|
||||||
|
@ -57,7 +52,37 @@ class StaticFilesHandler(WSGIHandler):
|
||||||
except Http404 as e:
|
except Http404 as e:
|
||||||
return response_for_exception(request, e)
|
return response_for_exception(request, e)
|
||||||
|
|
||||||
|
|
||||||
|
class StaticFilesHandler(StaticFilesHandlerMixin, WSGIHandler):
|
||||||
|
"""
|
||||||
|
WSGI middleware that intercepts calls to the static files directory, as
|
||||||
|
defined by the STATIC_URL setting, and serves those files.
|
||||||
|
"""
|
||||||
|
def __init__(self, application):
|
||||||
|
self.application = application
|
||||||
|
self.base_url = urlparse(self.get_base_url())
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
if not self._should_handle(get_path_info(environ)):
|
if not self._should_handle(get_path_info(environ)):
|
||||||
return self.application(environ, start_response)
|
return self.application(environ, start_response)
|
||||||
return super().__call__(environ, start_response)
|
return super().__call__(environ, start_response)
|
||||||
|
|
||||||
|
|
||||||
|
class ASGIStaticFilesHandler(StaticFilesHandlerMixin, ASGIHandler):
|
||||||
|
"""
|
||||||
|
ASGI application which wraps another and intercepts requests for static
|
||||||
|
files, passing them off to Django's static file serving.
|
||||||
|
"""
|
||||||
|
def __init__(self, application):
|
||||||
|
self.application = application
|
||||||
|
self.base_url = urlparse(self.get_base_url())
|
||||||
|
|
||||||
|
async def __call__(self, scope, receive, send):
|
||||||
|
# Only even look at HTTP requests
|
||||||
|
if scope['type'] == 'http' and self._should_handle(scope['path']):
|
||||||
|
# Serve static content
|
||||||
|
# (the one thing super() doesn't do is __call__, apparently)
|
||||||
|
return await super().__call__(scope, receive, send)
|
||||||
|
# Hand off to the main app
|
||||||
|
return await self.application(scope, receive, send)
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import django
|
||||||
|
from django.core.handlers.asgi import ASGIHandler
|
||||||
|
|
||||||
|
|
||||||
|
def get_asgi_application():
|
||||||
|
"""
|
||||||
|
The public interface to Django's ASGI support. Return an ASGI 3 callable.
|
||||||
|
|
||||||
|
Avoids making django.core.handlers.ASGIHandler a public API, in case the
|
||||||
|
internal implementation changes or moves in the future.
|
||||||
|
"""
|
||||||
|
django.setup(set_prefix=False)
|
||||||
|
return ASGIHandler()
|
|
@ -63,6 +63,11 @@ class RequestDataTooBig(SuspiciousOperation):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RequestAborted(Exception):
|
||||||
|
"""The request was closed before it was completed, or timed out."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PermissionDenied(Exception):
|
class PermissionDenied(Exception):
|
||||||
"""The user did not have permission to do that"""
|
"""The user did not have permission to do that"""
|
||||||
pass
|
pass
|
||||||
|
@ -181,3 +186,8 @@ class ValidationError(Exception):
|
||||||
class EmptyResultSet(Exception):
|
class EmptyResultSet(Exception):
|
||||||
"""A database query predicate is impossible."""
|
"""A database query predicate is impossible."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SynchronousOnlyOperation(Exception):
|
||||||
|
"""The user tried to call a sync-only function from an async context."""
|
||||||
|
pass
|
||||||
|
|
|
@ -0,0 +1,297 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core import signals
|
||||||
|
from django.core.exceptions import RequestAborted, RequestDataTooBig
|
||||||
|
from django.core.handlers import base
|
||||||
|
from django.http import (
|
||||||
|
FileResponse, HttpRequest, HttpResponse, HttpResponseBadRequest,
|
||||||
|
HttpResponseServerError, QueryDict, parse_cookie,
|
||||||
|
)
|
||||||
|
from django.urls import set_script_prefix
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
logger = logging.getLogger('django.request')
|
||||||
|
|
||||||
|
|
||||||
|
class ASGIRequest(HttpRequest):
|
||||||
|
"""
|
||||||
|
Custom request subclass that decodes from an ASGI-standard request dict
|
||||||
|
and wraps request body handling.
|
||||||
|
"""
|
||||||
|
# Number of seconds until a Request gives up on trying to read a request
|
||||||
|
# body and aborts.
|
||||||
|
body_receive_timeout = 60
|
||||||
|
|
||||||
|
def __init__(self, scope, body_file):
|
||||||
|
self.scope = scope
|
||||||
|
self._post_parse_error = False
|
||||||
|
self._read_started = False
|
||||||
|
self.resolver_match = None
|
||||||
|
self.script_name = self.scope.get('root_path', '')
|
||||||
|
if self.script_name and scope['path'].startswith(self.script_name):
|
||||||
|
# TODO: Better is-prefix checking, slash handling?
|
||||||
|
self.path_info = scope['path'][len(self.script_name):]
|
||||||
|
else:
|
||||||
|
self.path_info = scope['path']
|
||||||
|
# The Django path is different from ASGI scope path args, it should
|
||||||
|
# combine with script name.
|
||||||
|
if self.script_name:
|
||||||
|
self.path = '%s/%s' % (
|
||||||
|
self.script_name.rstrip('/'),
|
||||||
|
self.path_info.replace('/', '', 1),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.path = scope['path']
|
||||||
|
# HTTP basics.
|
||||||
|
self.method = self.scope['method'].upper()
|
||||||
|
# Ensure query string is encoded correctly.
|
||||||
|
query_string = self.scope.get('query_string', '')
|
||||||
|
if isinstance(query_string, bytes):
|
||||||
|
query_string = query_string.decode()
|
||||||
|
self.META = {
|
||||||
|
'REQUEST_METHOD': self.method,
|
||||||
|
'QUERY_STRING': query_string,
|
||||||
|
'SCRIPT_NAME': self.script_name,
|
||||||
|
'PATH_INFO': self.path_info,
|
||||||
|
# WSGI-expecting code will need these for a while
|
||||||
|
'wsgi.multithread': True,
|
||||||
|
'wsgi.multiprocess': True,
|
||||||
|
}
|
||||||
|
if self.scope.get('client'):
|
||||||
|
self.META['REMOTE_ADDR'] = self.scope['client'][0]
|
||||||
|
self.META['REMOTE_HOST'] = self.META['REMOTE_ADDR']
|
||||||
|
self.META['REMOTE_PORT'] = self.scope['client'][1]
|
||||||
|
if self.scope.get('server'):
|
||||||
|
self.META['SERVER_NAME'] = self.scope['server'][0]
|
||||||
|
self.META['SERVER_PORT'] = str(self.scope['server'][1])
|
||||||
|
else:
|
||||||
|
self.META['SERVER_NAME'] = 'unknown'
|
||||||
|
self.META['SERVER_PORT'] = '0'
|
||||||
|
# Headers go into META.
|
||||||
|
for name, value in self.scope.get('headers', []):
|
||||||
|
name = name.decode('latin1')
|
||||||
|
if name == 'content-length':
|
||||||
|
corrected_name = 'CONTENT_LENGTH'
|
||||||
|
elif name == 'content-type':
|
||||||
|
corrected_name = 'CONTENT_TYPE'
|
||||||
|
else:
|
||||||
|
corrected_name = 'HTTP_%s' % name.upper().replace('-', '_')
|
||||||
|
# HTTP/2 say only ASCII chars are allowed in headers, but decode
|
||||||
|
# latin1 just in case.
|
||||||
|
value = value.decode('latin1')
|
||||||
|
if corrected_name in self.META:
|
||||||
|
value = self.META[corrected_name] + ',' + value
|
||||||
|
self.META[corrected_name] = value
|
||||||
|
# Pull out request encoding, if provided.
|
||||||
|
self._set_content_type_params(self.META)
|
||||||
|
# Directly assign the body file to be our stream.
|
||||||
|
self._stream = body_file
|
||||||
|
# Other bits.
|
||||||
|
self.resolver_match = None
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def GET(self):
|
||||||
|
return QueryDict(self.META['QUERY_STRING'])
|
||||||
|
|
||||||
|
def _get_scheme(self):
|
||||||
|
return self.scope.get('scheme') or super()._get_scheme()
|
||||||
|
|
||||||
|
def _get_post(self):
|
||||||
|
if not hasattr(self, '_post'):
|
||||||
|
self._load_post_and_files()
|
||||||
|
return self._post
|
||||||
|
|
||||||
|
def _set_post(self, post):
|
||||||
|
self._post = post
|
||||||
|
|
||||||
|
def _get_files(self):
|
||||||
|
if not hasattr(self, '_files'):
|
||||||
|
self._load_post_and_files()
|
||||||
|
return self._files
|
||||||
|
|
||||||
|
POST = property(_get_post, _set_post)
|
||||||
|
FILES = property(_get_files)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def COOKIES(self):
|
||||||
|
return parse_cookie(self.META.get('HTTP_COOKIE', ''))
|
||||||
|
|
||||||
|
|
||||||
|
class ASGIHandler(base.BaseHandler):
|
||||||
|
"""Handler for ASGI requests."""
|
||||||
|
request_class = ASGIRequest
|
||||||
|
# Size to chunk response bodies into for multiple response messages.
|
||||||
|
chunk_size = 2 ** 16
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ASGIHandler, self).__init__()
|
||||||
|
self.load_middleware()
|
||||||
|
|
||||||
|
async def __call__(self, scope, receive, send):
|
||||||
|
"""
|
||||||
|
Async entrypoint - parses the request and hands off to get_response.
|
||||||
|
"""
|
||||||
|
# Serve only HTTP connections.
|
||||||
|
# FIXME: Allow to override this.
|
||||||
|
if scope['type'] != 'http':
|
||||||
|
raise ValueError(
|
||||||
|
'Django can only handle ASGI/HTTP connections, not %s'
|
||||||
|
% scope['type']
|
||||||
|
)
|
||||||
|
# Receive the HTTP request body as a stream object.
|
||||||
|
try:
|
||||||
|
body_file = await self.read_body(receive)
|
||||||
|
except RequestAborted:
|
||||||
|
return
|
||||||
|
# Request is complete and can be served.
|
||||||
|
set_script_prefix(self.get_script_prefix(scope))
|
||||||
|
await sync_to_async(signals.request_started.send)(sender=self.__class__, scope=scope)
|
||||||
|
# Get the request and check for basic issues.
|
||||||
|
request, error_response = self.create_request(scope, body_file)
|
||||||
|
if request is None:
|
||||||
|
await self.send_response(error_response, send)
|
||||||
|
return
|
||||||
|
# Get the response, using a threadpool via sync_to_async, if needed.
|
||||||
|
if asyncio.iscoroutinefunction(self.get_response):
|
||||||
|
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__
|
||||||
|
# Increase chunk size on file responses (ASGI servers handles low-level
|
||||||
|
# chunking).
|
||||||
|
if isinstance(response, FileResponse):
|
||||||
|
response.block_size = self.chunk_size
|
||||||
|
# Send the response.
|
||||||
|
await self.send_response(response, send)
|
||||||
|
|
||||||
|
async def read_body(self, receive):
|
||||||
|
"""Reads a HTTP body from an ASGI connection."""
|
||||||
|
# Use the tempfile that auto rolls-over to a disk file as it fills up,
|
||||||
|
# if a maximum in-memory size is set. Otherwise use a BytesIO object.
|
||||||
|
if settings.FILE_UPLOAD_MAX_MEMORY_SIZE is None:
|
||||||
|
body_file = BytesIO()
|
||||||
|
else:
|
||||||
|
body_file = tempfile.SpooledTemporaryFile(max_size=settings.FILE_UPLOAD_MAX_MEMORY_SIZE, mode='w+b')
|
||||||
|
while True:
|
||||||
|
message = await receive()
|
||||||
|
if message['type'] == 'http.disconnect':
|
||||||
|
# Early client disconnect.
|
||||||
|
raise RequestAborted()
|
||||||
|
# Add a body chunk from the message, if provided.
|
||||||
|
if 'body' in message:
|
||||||
|
body_file.write(message['body'])
|
||||||
|
# Quit out if that's the end.
|
||||||
|
if not message.get('more_body', False):
|
||||||
|
break
|
||||||
|
body_file.seek(0)
|
||||||
|
return body_file
|
||||||
|
|
||||||
|
def create_request(self, scope, body_file):
|
||||||
|
"""
|
||||||
|
Create the Request object and returns either (request, None) or
|
||||||
|
(None, response) if there is an error response.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.request_class(scope, body_file), None
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
logger.warning(
|
||||||
|
'Bad Request (UnicodeDecodeError)',
|
||||||
|
exc_info=sys.exc_info(),
|
||||||
|
extra={'status_code': 400},
|
||||||
|
)
|
||||||
|
return None, HttpResponseBadRequest()
|
||||||
|
except RequestDataTooBig:
|
||||||
|
return None, HttpResponse('413 Payload too large', status=413)
|
||||||
|
|
||||||
|
def handle_uncaught_exception(self, request, resolver, exc_info):
|
||||||
|
"""Last-chance handler for exceptions."""
|
||||||
|
# There's no WSGI server to catch the exception further up
|
||||||
|
# if this fails, so translate it into a plain text response.
|
||||||
|
try:
|
||||||
|
return super().handle_uncaught_exception(request, resolver, exc_info)
|
||||||
|
except Exception:
|
||||||
|
return HttpResponseServerError(
|
||||||
|
traceback.format_exc() if settings.DEBUG else 'Internal Server Error',
|
||||||
|
content_type='text/plain',
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_response(self, response, send):
|
||||||
|
"""Encode and send a response out over ASGI."""
|
||||||
|
# Collect cookies into headers. Have to preserve header case as there
|
||||||
|
# are some non-RFC compliant clients that require e.g. Content-Type.
|
||||||
|
response_headers = []
|
||||||
|
for header, value in response.items():
|
||||||
|
if isinstance(header, str):
|
||||||
|
header = header.encode('ascii')
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.encode('latin1')
|
||||||
|
response_headers.append((bytes(header), bytes(value)))
|
||||||
|
for c in response.cookies.values():
|
||||||
|
response_headers.append(
|
||||||
|
(b'Set-Cookie', c.output(header='').encode('ascii').strip())
|
||||||
|
)
|
||||||
|
# Initial response message.
|
||||||
|
await send({
|
||||||
|
'type': 'http.response.start',
|
||||||
|
'status': response.status_code,
|
||||||
|
'headers': response_headers,
|
||||||
|
})
|
||||||
|
# Streaming responses need to be pinned to their iterator.
|
||||||
|
if response.streaming:
|
||||||
|
# Access `__iter__` and not `streaming_content` directly in case
|
||||||
|
# it has been overridden in a subclass.
|
||||||
|
for part in response:
|
||||||
|
for chunk, _ in self.chunk_bytes(part):
|
||||||
|
await send({
|
||||||
|
'type': 'http.response.body',
|
||||||
|
'body': chunk,
|
||||||
|
# Ignore "more" as there may be more parts; instead,
|
||||||
|
# use an empty final closing message with False.
|
||||||
|
'more_body': True,
|
||||||
|
})
|
||||||
|
# Final closing message.
|
||||||
|
await send({'type': 'http.response.body'})
|
||||||
|
# Other responses just need chunking.
|
||||||
|
else:
|
||||||
|
# Yield chunks of response.
|
||||||
|
for chunk, last in self.chunk_bytes(response.content):
|
||||||
|
await send({
|
||||||
|
'type': 'http.response.body',
|
||||||
|
'body': chunk,
|
||||||
|
'more_body': not last,
|
||||||
|
})
|
||||||
|
response.close()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def chunk_bytes(cls, data):
|
||||||
|
"""
|
||||||
|
Chunks some data up so it can be sent in reasonable size messages.
|
||||||
|
Yields (chunk, last_chunk) tuples.
|
||||||
|
"""
|
||||||
|
position = 0
|
||||||
|
if not data:
|
||||||
|
yield data, True
|
||||||
|
return
|
||||||
|
while position < len(data):
|
||||||
|
yield (
|
||||||
|
data[position:position + cls.chunk_size],
|
||||||
|
(position + cls.chunk_size) >= len(data),
|
||||||
|
)
|
||||||
|
position += cls.chunk_size
|
||||||
|
|
||||||
|
def get_script_prefix(self, scope):
|
||||||
|
"""
|
||||||
|
Return the script prefix to use from either the scope or a setting.
|
||||||
|
"""
|
||||||
|
if settings.FORCE_SCRIPT_NAME:
|
||||||
|
return settings.FORCE_SCRIPT_NAME
|
||||||
|
return scope.get('root_path', '') or ''
|
|
@ -1,6 +1,6 @@
|
||||||
from django.dispatch import Signal
|
from django.dispatch import Signal
|
||||||
|
|
||||||
request_started = Signal(providing_args=["environ"])
|
request_started = Signal(providing_args=["environ", "scope"])
|
||||||
request_finished = Signal()
|
request_finished = Signal()
|
||||||
got_request_exception = Signal(providing_args=["request"])
|
got_request_exception = Signal(providing_args=["request"])
|
||||||
setting_changed = Signal(providing_args=["setting", "value", "enter"])
|
setting_changed = Signal(providing_args=["setting", "value", "enter"])
|
||||||
|
|
|
@ -17,6 +17,7 @@ from django.db.backends.signals import connection_created
|
||||||
from django.db.transaction import TransactionManagementError
|
from django.db.transaction import TransactionManagementError
|
||||||
from django.db.utils import DatabaseError, DatabaseErrorWrapper
|
from django.db.utils import DatabaseError, DatabaseErrorWrapper
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.asyncio import async_unsafe
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
NO_DB_ALIAS = '__no_db__'
|
NO_DB_ALIAS = '__no_db__'
|
||||||
|
@ -177,6 +178,7 @@ class BaseDatabaseWrapper:
|
||||||
|
|
||||||
# ##### Backend-specific methods for creating connections #####
|
# ##### Backend-specific methods for creating connections #####
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connect to the database. Assume that the connection is closed."""
|
"""Connect to the database. Assume that the connection is closed."""
|
||||||
# Check for invalid configurations.
|
# Check for invalid configurations.
|
||||||
|
@ -210,6 +212,7 @@ class BaseDatabaseWrapper:
|
||||||
"Connection '%s' cannot set TIME_ZONE because its engine "
|
"Connection '%s' cannot set TIME_ZONE because its engine "
|
||||||
"handles time zones conversions natively." % self.alias)
|
"handles time zones conversions natively." % self.alias)
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def ensure_connection(self):
|
def ensure_connection(self):
|
||||||
"""Guarantee that a connection to the database is established."""
|
"""Guarantee that a connection to the database is established."""
|
||||||
if self.connection is None:
|
if self.connection is None:
|
||||||
|
@ -251,10 +254,12 @@ class BaseDatabaseWrapper:
|
||||||
|
|
||||||
# ##### Generic wrappers for PEP-249 connection methods #####
|
# ##### Generic wrappers for PEP-249 connection methods #####
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def cursor(self):
|
def cursor(self):
|
||||||
"""Create a cursor, opening a connection if necessary."""
|
"""Create a cursor, opening a connection if necessary."""
|
||||||
return self._cursor()
|
return self._cursor()
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def commit(self):
|
def commit(self):
|
||||||
"""Commit a transaction and reset the dirty flag."""
|
"""Commit a transaction and reset the dirty flag."""
|
||||||
self.validate_thread_sharing()
|
self.validate_thread_sharing()
|
||||||
|
@ -264,6 +269,7 @@ class BaseDatabaseWrapper:
|
||||||
self.errors_occurred = False
|
self.errors_occurred = False
|
||||||
self.run_commit_hooks_on_set_autocommit_on = True
|
self.run_commit_hooks_on_set_autocommit_on = True
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def rollback(self):
|
def rollback(self):
|
||||||
"""Roll back a transaction and reset the dirty flag."""
|
"""Roll back a transaction and reset the dirty flag."""
|
||||||
self.validate_thread_sharing()
|
self.validate_thread_sharing()
|
||||||
|
@ -274,6 +280,7 @@ class BaseDatabaseWrapper:
|
||||||
self.needs_rollback = False
|
self.needs_rollback = False
|
||||||
self.run_on_commit = []
|
self.run_on_commit = []
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the connection to the database."""
|
"""Close the connection to the database."""
|
||||||
self.validate_thread_sharing()
|
self.validate_thread_sharing()
|
||||||
|
@ -313,6 +320,7 @@ class BaseDatabaseWrapper:
|
||||||
|
|
||||||
# ##### Generic savepoint management methods #####
|
# ##### Generic savepoint management methods #####
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def savepoint(self):
|
def savepoint(self):
|
||||||
"""
|
"""
|
||||||
Create a savepoint inside the current transaction. Return an
|
Create a savepoint inside the current transaction. Return an
|
||||||
|
@ -333,6 +341,7 @@ class BaseDatabaseWrapper:
|
||||||
|
|
||||||
return sid
|
return sid
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def savepoint_rollback(self, sid):
|
def savepoint_rollback(self, sid):
|
||||||
"""
|
"""
|
||||||
Roll back to a savepoint. Do nothing if savepoints are not supported.
|
Roll back to a savepoint. Do nothing if savepoints are not supported.
|
||||||
|
@ -348,6 +357,7 @@ class BaseDatabaseWrapper:
|
||||||
(sids, func) for (sids, func) in self.run_on_commit if sid not in sids
|
(sids, func) for (sids, func) in self.run_on_commit if sid not in sids
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def savepoint_commit(self, sid):
|
def savepoint_commit(self, sid):
|
||||||
"""
|
"""
|
||||||
Release a savepoint. Do nothing if savepoints are not supported.
|
Release a savepoint. Do nothing if savepoints are not supported.
|
||||||
|
@ -358,6 +368,7 @@ class BaseDatabaseWrapper:
|
||||||
self.validate_thread_sharing()
|
self.validate_thread_sharing()
|
||||||
self._savepoint_commit(sid)
|
self._savepoint_commit(sid)
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def clean_savepoints(self):
|
def clean_savepoints(self):
|
||||||
"""
|
"""
|
||||||
Reset the counter used to generate unique savepoint ids in this thread.
|
Reset the counter used to generate unique savepoint ids in this thread.
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import utils
|
from django.db import utils
|
||||||
from django.db.backends import utils as backend_utils
|
from django.db.backends import utils as backend_utils
|
||||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||||
|
from django.utils.asyncio import async_unsafe
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -223,6 +224,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
kwargs.update(options)
|
kwargs.update(options)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def get_new_connection(self, conn_params):
|
def get_new_connection(self, conn_params):
|
||||||
return Database.connect(**conn_params)
|
return Database.connect(**conn_params)
|
||||||
|
|
||||||
|
@ -242,6 +244,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
with self.cursor() as cursor:
|
with self.cursor() as cursor:
|
||||||
cursor.execute('; '.join(assignments))
|
cursor.execute('; '.join(assignments))
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def create_cursor(self, name=None):
|
def create_cursor(self, name=None):
|
||||||
cursor = self.connection.cursor()
|
cursor = self.connection.cursor()
|
||||||
return CursorWrapper(cursor)
|
return CursorWrapper(cursor)
|
||||||
|
|
|
@ -13,6 +13,7 @@ from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import utils
|
from django.db import utils
|
||||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||||
|
from django.utils.asyncio import async_unsafe
|
||||||
from django.utils.encoding import force_bytes, force_str
|
from django.utils.encoding import force_bytes, force_str
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
@ -221,6 +222,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
del conn_params['use_returning_into']
|
del conn_params['use_returning_into']
|
||||||
return conn_params
|
return conn_params
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def get_new_connection(self, conn_params):
|
def get_new_connection(self, conn_params):
|
||||||
return Database.connect(
|
return Database.connect(
|
||||||
user=self.settings_dict['USER'],
|
user=self.settings_dict['USER'],
|
||||||
|
@ -269,6 +271,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
if not self.get_autocommit():
|
if not self.get_autocommit():
|
||||||
self.commit()
|
self.commit()
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def create_cursor(self, name=None):
|
def create_cursor(self, name=None):
|
||||||
return FormatStylePlaceholderCursor(self.connection)
|
return FormatStylePlaceholderCursor(self.connection)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ PostgreSQL database backend for Django.
|
||||||
Requires psycopg 2: http://initd.org/projects/psycopg2
|
Requires psycopg 2: http://initd.org/projects/psycopg2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ from django.db.backends.utils import (
|
||||||
CursorDebugWrapper as BaseCursorDebugWrapper,
|
CursorDebugWrapper as BaseCursorDebugWrapper,
|
||||||
)
|
)
|
||||||
from django.db.utils import DatabaseError as WrappedDatabaseError
|
from django.db.utils import DatabaseError as WrappedDatabaseError
|
||||||
|
from django.utils.asyncio import async_unsafe
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
from django.utils.version import get_version_tuple
|
from django.utils.version import get_version_tuple
|
||||||
|
@ -177,6 +179,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
conn_params['port'] = settings_dict['PORT']
|
conn_params['port'] = settings_dict['PORT']
|
||||||
return conn_params
|
return conn_params
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def get_new_connection(self, conn_params):
|
def get_new_connection(self, conn_params):
|
||||||
connection = Database.connect(**conn_params)
|
connection = Database.connect(**conn_params)
|
||||||
|
|
||||||
|
@ -217,6 +220,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
if not self.get_autocommit():
|
if not self.get_autocommit():
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def create_cursor(self, name=None):
|
def create_cursor(self, name=None):
|
||||||
if name:
|
if name:
|
||||||
# In autocommit mode, the cursor will be used outside of a
|
# In autocommit mode, the cursor will be used outside of a
|
||||||
|
@ -227,12 +231,34 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
|
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
|
||||||
return cursor
|
return cursor
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def chunked_cursor(self):
|
def chunked_cursor(self):
|
||||||
self._named_cursor_idx += 1
|
self._named_cursor_idx += 1
|
||||||
|
# Get the current async task
|
||||||
|
# Note that right now this is behind @async_unsafe, so this is
|
||||||
|
# unreachable, but in future we'll start loosening this restriction.
|
||||||
|
# For now, it's here so that every use of "threading" is
|
||||||
|
# also async-compatible.
|
||||||
|
try:
|
||||||
|
if hasattr(asyncio, 'current_task'):
|
||||||
|
# Python 3.7 and up
|
||||||
|
current_task = asyncio.current_task()
|
||||||
|
else:
|
||||||
|
# Python 3.6
|
||||||
|
current_task = asyncio.Task.current_task()
|
||||||
|
except RuntimeError:
|
||||||
|
current_task = None
|
||||||
|
# Current task can be none even if the current_task call didn't error
|
||||||
|
if current_task:
|
||||||
|
task_ident = str(id(current_task))
|
||||||
|
else:
|
||||||
|
task_ident = 'sync'
|
||||||
|
# Use that and the thread ident to get a unique name
|
||||||
return self._cursor(
|
return self._cursor(
|
||||||
name='_django_curs_%d_%d' % (
|
name='_django_curs_%d_%s_%d' % (
|
||||||
# Avoid reusing name in other threads
|
# Avoid reusing name in other threads / tasks
|
||||||
threading.current_thread().ident,
|
threading.current_thread().ident,
|
||||||
|
task_ident,
|
||||||
self._named_cursor_idx,
|
self._named_cursor_idx,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,6 +20,7 @@ from django.db import utils
|
||||||
from django.db.backends import utils as backend_utils
|
from django.db.backends import utils as backend_utils
|
||||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.asyncio import async_unsafe
|
||||||
from django.utils.dateparse import parse_datetime, parse_time
|
from django.utils.dateparse import parse_datetime, parse_time
|
||||||
from django.utils.duration import duration_microseconds
|
from django.utils.duration import duration_microseconds
|
||||||
|
|
||||||
|
@ -191,6 +192,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
kwargs.update({'check_same_thread': False, 'uri': True})
|
kwargs.update({'check_same_thread': False, 'uri': True})
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def get_new_connection(self, conn_params):
|
def get_new_connection(self, conn_params):
|
||||||
conn = Database.connect(**conn_params)
|
conn = Database.connect(**conn_params)
|
||||||
conn.create_function("django_date_extract", 2, _sqlite_datetime_extract)
|
conn.create_function("django_date_extract", 2, _sqlite_datetime_extract)
|
||||||
|
@ -248,6 +250,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
def create_cursor(self, name=None):
|
def create_cursor(self, name=None):
|
||||||
return self.connection.cursor(factory=SQLiteCursorWrapper)
|
return self.connection.cursor(factory=SQLiteCursorWrapper)
|
||||||
|
|
||||||
|
@async_unsafe
|
||||||
def close(self):
|
def close(self):
|
||||||
self.validate_thread_sharing()
|
self.validate_thread_sharing()
|
||||||
# If database is in memory, closing the connection destroys the
|
# If database is in memory, closing the connection destroys the
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import local
|
|
||||||
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
@ -139,7 +140,12 @@ class ConnectionHandler:
|
||||||
like settings.DATABASES).
|
like settings.DATABASES).
|
||||||
"""
|
"""
|
||||||
self._databases = databases
|
self._databases = databases
|
||||||
self._connections = local()
|
# Connections needs to still be an actual thread local, as it's truly
|
||||||
|
# thread-critical. Database backends should use @async_unsafe to protect
|
||||||
|
# their code from async contexts, but this will give those contexts
|
||||||
|
# separate connections in case it's needed as well. There's no cleanup
|
||||||
|
# after async contexts, though, so we don't allow that if we can help it.
|
||||||
|
self._connections = Local(thread_critical=True)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def databases(self):
|
def databases(self):
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.signals import setting_changed
|
from django.core.signals import setting_changed
|
||||||
|
@ -26,7 +27,7 @@ COMPLEX_OVERRIDE_SETTINGS = {'DATABASES'}
|
||||||
def clear_cache_handlers(**kwargs):
|
def clear_cache_handlers(**kwargs):
|
||||||
if kwargs['setting'] == 'CACHES':
|
if kwargs['setting'] == 'CACHES':
|
||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
caches._caches = threading.local()
|
caches._caches = Local()
|
||||||
|
|
||||||
|
|
||||||
@receiver(setting_changed)
|
@receiver(setting_changed)
|
||||||
|
@ -113,7 +114,7 @@ def language_changed(**kwargs):
|
||||||
if kwargs['setting'] in {'LANGUAGES', 'LANGUAGE_CODE', 'LOCALE_PATHS'}:
|
if kwargs['setting'] in {'LANGUAGES', 'LANGUAGE_CODE', 'LOCALE_PATHS'}:
|
||||||
from django.utils.translation import trans_real
|
from django.utils.translation import trans_real
|
||||||
trans_real._default = None
|
trans_real._default = None
|
||||||
trans_real._active = threading.local()
|
trans_real._active = Local()
|
||||||
if kwargs['setting'] in {'LANGUAGES', 'LOCALE_PATHS'}:
|
if kwargs['setting'] in {'LANGUAGES', 'LOCALE_PATHS'}:
|
||||||
from django.utils.translation import trans_real
|
from django.utils.translation import trans_real
|
||||||
trans_real._translations = {}
|
trans_real._translations = {}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from threading import local
|
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.utils.encoding import iri_to_uri
|
from django.utils.encoding import iri_to_uri
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
from django.utils.translation import override
|
from django.utils.translation import override
|
||||||
|
@ -12,10 +13,10 @@ from .utils import get_callable
|
||||||
# SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
|
# SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
|
||||||
# the current thread (which is the only one we ever access), it is assumed to
|
# the current thread (which is the only one we ever access), it is assumed to
|
||||||
# be empty.
|
# be empty.
|
||||||
_prefixes = local()
|
_prefixes = Local()
|
||||||
|
|
||||||
# Overridden URLconfs for each thread are stored here.
|
# Overridden URLconfs for each thread are stored here.
|
||||||
_urlconfs = local()
|
_urlconfs = Local()
|
||||||
|
|
||||||
|
|
||||||
def resolve(path, urlconf=None):
|
def resolve(path, urlconf=None):
|
||||||
|
|
|
@ -8,10 +8,11 @@ attributes of the resolved URL match.
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.checks import Error, Warning
|
from django.core.checks import Error, Warning
|
||||||
from django.core.checks.urls import check_resolver
|
from django.core.checks.urls import check_resolver
|
||||||
|
@ -380,7 +381,7 @@ class URLResolver:
|
||||||
# urlpatterns
|
# urlpatterns
|
||||||
self._callback_strs = set()
|
self._callback_strs = set()
|
||||||
self._populated = False
|
self._populated = False
|
||||||
self._local = threading.local()
|
self._local = Local()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if isinstance(self.urlconf_name, list) and self.urlconf_name:
|
if isinstance(self.urlconf_name, list) and self.urlconf_name:
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from django.core.exceptions import SynchronousOnlyOperation
|
||||||
|
|
||||||
|
|
||||||
|
def async_unsafe(message):
|
||||||
|
"""
|
||||||
|
Decorator to mark functions as async-unsafe. Someone trying to access
|
||||||
|
the function while in an async context will get an error message.
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
# Detect a running event loop in this thread.
|
||||||
|
try:
|
||||||
|
event_loop = asyncio.get_event_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if event_loop.is_running():
|
||||||
|
raise SynchronousOnlyOperation(message)
|
||||||
|
# Pass onwards.
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return inner
|
||||||
|
# If the message is actually a function, then be a no-arguments decorator.
|
||||||
|
if callable(message):
|
||||||
|
func = message
|
||||||
|
message = 'You cannot call this from an async context - use a thread or sync_to_async.'
|
||||||
|
return decorator(func)
|
||||||
|
else:
|
||||||
|
return decorator
|
|
@ -6,9 +6,9 @@ import functools
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import ContextDecorator
|
from contextlib import ContextDecorator
|
||||||
from datetime import datetime, timedelta, timezone, tzinfo
|
from datetime import datetime, timedelta, timezone, tzinfo
|
||||||
from threading import local
|
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.deprecation import RemovedInDjango31Warning
|
from django.utils.deprecation import RemovedInDjango31Warning
|
||||||
|
@ -89,7 +89,7 @@ def get_default_timezone_name():
|
||||||
return _get_timezone_name(get_default_timezone())
|
return _get_timezone_name(get_default_timezone())
|
||||||
|
|
||||||
|
|
||||||
_active = local()
|
_active = Local()
|
||||||
|
|
||||||
|
|
||||||
def get_current_timezone():
|
def get_current_timezone():
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import threading
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,5 +26,5 @@ def translation_file_changed(sender, file_path, **kwargs):
|
||||||
gettext._translations = {}
|
gettext._translations = {}
|
||||||
trans_real._translations = {}
|
trans_real._translations = {}
|
||||||
trans_real._default = None
|
trans_real._default = None
|
||||||
trans_real._active = threading.local()
|
trans_real._active = Local()
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -5,7 +5,8 @@ import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from threading import local
|
|
||||||
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -20,7 +21,7 @@ from . import to_language, to_locale
|
||||||
# Translations are cached in a dictionary for every language.
|
# Translations are cached in a dictionary for every language.
|
||||||
# The active translations are stored by threadid to make them thread local.
|
# The active translations are stored by threadid to make them thread local.
|
||||||
_translations = {}
|
_translations = {}
|
||||||
_active = local()
|
_active = Local()
|
||||||
|
|
||||||
# The default translation is based on the settings file.
|
# The default translation is based on the settings file.
|
||||||
_default = None
|
_default = None
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
=============================
|
||||||
|
How to use Django with Daphne
|
||||||
|
=============================
|
||||||
|
|
||||||
|
.. highlight:: bash
|
||||||
|
|
||||||
|
Daphne_ is a pure-Python ASGI server for UNIX, maintained by members of the
|
||||||
|
Django project. It acts as the reference server for ASGI.
|
||||||
|
|
||||||
|
.. _Daphne: https://pypi.org/project/daphne/
|
||||||
|
|
||||||
|
Installing Daphne
|
||||||
|
===================
|
||||||
|
|
||||||
|
You can install Daphne with ``pip``::
|
||||||
|
|
||||||
|
python -m pip install daphne
|
||||||
|
|
||||||
|
Running Django in Daphne
|
||||||
|
========================
|
||||||
|
|
||||||
|
When Daphne is installed, a ``daphne`` command is available which starts the
|
||||||
|
Daphne server process. At its simplest, Daphne needs to be called with the
|
||||||
|
location of a module containing an ASGI application object, followed by what
|
||||||
|
the application is called (separated by a colon).
|
||||||
|
|
||||||
|
For a typical Django project, invoking Daphne would look like::
|
||||||
|
|
||||||
|
daphne myproject.asgi:application
|
||||||
|
|
||||||
|
This will start one process listening on ``127.0.0.1:8000``. It requires that
|
||||||
|
your project be on the Python path; to ensure that run this command from the
|
||||||
|
same directory as your ``manage.py`` file.
|
|
@ -0,0 +1,71 @@
|
||||||
|
=======================
|
||||||
|
How to deploy with ASGI
|
||||||
|
=======================
|
||||||
|
|
||||||
|
As well as WSGI, Django also supports deploying on ASGI_, the emerging Python
|
||||||
|
standard for asynchronous web servers and applications.
|
||||||
|
|
||||||
|
.. _ASGI: https://asgi.readthedocs.io/en/latest/
|
||||||
|
|
||||||
|
Django's :djadmin:`startproject` management command sets up a default ASGI
|
||||||
|
configuration for you, which you can tweak as needed for your project, and
|
||||||
|
direct any ASGI-compliant application server to use.
|
||||||
|
|
||||||
|
Django includes getting-started documentation for the following ASGI servers:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
daphne
|
||||||
|
uvicorn
|
||||||
|
|
||||||
|
The ``application`` object
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Like WSGI, ASGI has you supply an ``application`` callable which
|
||||||
|
the application server uses to communicate with your code. It's commonly
|
||||||
|
provided as an object named ``application`` in a Python module accessible to
|
||||||
|
the server.
|
||||||
|
|
||||||
|
The :djadmin:`startproject` command creates a file
|
||||||
|
:file:`<project_name>/asgi.py` that contains such an ``application`` callable.
|
||||||
|
|
||||||
|
It's not used by the development server (``runserver``), but can be used by
|
||||||
|
any ASGI server either in development or in production.
|
||||||
|
|
||||||
|
ASGI servers usually take the path to the application callable as a string;
|
||||||
|
for most Django projects, this will look like ``myproject.asgi:application``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
While Django's default ASGI handler will run all your code in a synchronous
|
||||||
|
thread, if you choose to run your own async handler you must be aware of
|
||||||
|
async-safety.
|
||||||
|
|
||||||
|
Do not call blocking synchronous functions or libraries in any async code.
|
||||||
|
Django prevents you from doing this with the parts of Django that are not
|
||||||
|
async-safe, but the same may not be true of third-party apps or Python
|
||||||
|
libraries.
|
||||||
|
|
||||||
|
Configuring the settings module
|
||||||
|
===============================
|
||||||
|
|
||||||
|
When the ASGI server loads your application, Django needs to import the
|
||||||
|
settings module — that's where your entire application is defined.
|
||||||
|
|
||||||
|
Django uses the :envvar:`DJANGO_SETTINGS_MODULE` environment variable to locate
|
||||||
|
the appropriate settings module. It must contain the dotted path to the
|
||||||
|
settings module. You can use a different value for development and production;
|
||||||
|
it all depends on how you organize your settings.
|
||||||
|
|
||||||
|
If this variable isn't set, the default :file:`asgi.py` sets it to
|
||||||
|
``mysite.settings``, where ``mysite`` is the name of your project.
|
||||||
|
|
||||||
|
Applying ASGI middleware
|
||||||
|
========================
|
||||||
|
|
||||||
|
To apply ASGI middleware, or to embed Django in another ASGI application, you
|
||||||
|
can wrap Django's ``application`` object in the ``asgi.py`` file. For example::
|
||||||
|
|
||||||
|
from some_asgi_library import AmazingMiddleware
|
||||||
|
application = AmazingMiddleware(application)
|
|
@ -0,0 +1,35 @@
|
||||||
|
==============================
|
||||||
|
How to use Django with Uvicorn
|
||||||
|
==============================
|
||||||
|
|
||||||
|
.. highlight:: bash
|
||||||
|
|
||||||
|
Uvicorn_ is an ASGI server based on ``uvloop`` and ``httptools``, with an
|
||||||
|
emphasis on speed.
|
||||||
|
|
||||||
|
Installing Uvicorn
|
||||||
|
==================
|
||||||
|
|
||||||
|
You can install Uvicorn with ``pip``::
|
||||||
|
|
||||||
|
python -m pip install uvicorn
|
||||||
|
|
||||||
|
Running Django in Uvicorn
|
||||||
|
=========================
|
||||||
|
|
||||||
|
When Uvicorn is installed, a ``uvicorn`` command is available which runs ASGI
|
||||||
|
applications. Uvicorn needs to be called with the location of a module
|
||||||
|
containing a ASGI application object, followed by what the application is
|
||||||
|
called (separated by a colon).
|
||||||
|
|
||||||
|
For a typical Django project, invoking Uvicorn would look like::
|
||||||
|
|
||||||
|
uvicorn myproject.asgi:application
|
||||||
|
|
||||||
|
This will start one process listening on ``127.0.0.1:8000``. It requires that
|
||||||
|
your project be on the Python path; to ensure that run this command from the
|
||||||
|
same directory as your ``manage.py`` file.
|
||||||
|
|
||||||
|
For more advanced usage, please read the `Uvicorn documentation <Uvicorn_>`_.
|
||||||
|
|
||||||
|
.. _Uvicorn: https://www.uvicorn.org/
|
|
@ -2,16 +2,21 @@
|
||||||
Deploying Django
|
Deploying Django
|
||||||
================
|
================
|
||||||
|
|
||||||
Django's chock-full of shortcuts to make Web developer's lives easier, but all
|
Django is full of shortcuts to make Web developers' lives easier, but all
|
||||||
those tools are of no use if you can't easily deploy your sites. Since Django's
|
those tools are of no use if you can't easily deploy your sites. Since Django's
|
||||||
inception, ease of deployment has been a major goal.
|
inception, ease of deployment has been a major goal.
|
||||||
|
|
||||||
|
This section contains guides to the two main ways to deploy Django. WSGI is the
|
||||||
|
main Python standard for communicating between Web servers and applications,
|
||||||
|
but it only supports synchronous code.
|
||||||
|
|
||||||
|
ASGI is the new, asynchronous-friendly standard that will allow your Django
|
||||||
|
site to use asynchronous Python features, and asynchronous Django features as
|
||||||
|
they are developed.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
wsgi/index
|
wsgi/index
|
||||||
|
asgi/index
|
||||||
checklist
|
checklist
|
||||||
|
|
||||||
If you're new to deploying Django and/or Python, we'd recommend you try
|
|
||||||
:doc:`mod_wsgi </howto/deployment/wsgi/modwsgi>` first. In most cases it'll be
|
|
||||||
the easiest, fastest, and most stable deployment choice.
|
|
||||||
|
|
|
@ -226,6 +226,7 @@ testing of Django applications:
|
||||||
* **Deployment:**
|
* **Deployment:**
|
||||||
:doc:`Overview <howto/deployment/index>` |
|
:doc:`Overview <howto/deployment/index>` |
|
||||||
:doc:`WSGI servers <howto/deployment/wsgi/index>` |
|
:doc:`WSGI servers <howto/deployment/wsgi/index>` |
|
||||||
|
:doc:`ASGI servers <howto/deployment/asgi/index>` |
|
||||||
:doc:`Deploying static files <howto/static-files/deployment>` |
|
:doc:`Deploying static files <howto/static-files/deployment>` |
|
||||||
:doc:`Tracking code errors by email <howto/error-reporting>`
|
:doc:`Tracking code errors by email <howto/error-reporting>`
|
||||||
|
|
||||||
|
|
|
@ -262,6 +262,7 @@ If you want to run the full suite of tests, you'll need to install a number of
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
||||||
* argon2-cffi_ 16.1.0+
|
* argon2-cffi_ 16.1.0+
|
||||||
|
* asgiref_ (required)
|
||||||
* bcrypt_
|
* bcrypt_
|
||||||
* docutils_
|
* docutils_
|
||||||
* geoip2_
|
* geoip2_
|
||||||
|
@ -306,6 +307,7 @@ To run some of the autoreload tests, you'll need to install the Watchman_
|
||||||
service.
|
service.
|
||||||
|
|
||||||
.. _argon2-cffi: https://pypi.org/project/argon2_cffi/
|
.. _argon2-cffi: https://pypi.org/project/argon2_cffi/
|
||||||
|
.. _asgiref: https://pypi.org/project/asgiref/
|
||||||
.. _bcrypt: https://pypi.org/project/bcrypt/
|
.. _bcrypt: https://pypi.org/project/bcrypt/
|
||||||
.. _docutils: https://pypi.org/project/docutils/
|
.. _docutils: https://pypi.org/project/docutils/
|
||||||
.. _geoip2: https://pypi.org/project/geoip2/
|
.. _geoip2: https://pypi.org/project/geoip2/
|
||||||
|
|
|
@ -162,6 +162,40 @@ or model are classified as ``NON_FIELD_ERRORS``. This constant is used
|
||||||
as a key in dictionaries that otherwise map fields to their respective
|
as a key in dictionaries that otherwise map fields to their respective
|
||||||
list of errors.
|
list of errors.
|
||||||
|
|
||||||
|
``RequestAborted``
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. exception:: RequestAborted
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
|
||||||
|
The :exc:`RequestAborted` exception is raised when a HTTP body being read
|
||||||
|
in by the handler is cut off midstream and the client connection closes,
|
||||||
|
or when the client does not send data and hits a timeout where the server
|
||||||
|
closes the connection.
|
||||||
|
|
||||||
|
It is internal to the HTTP handler modules and you are unlikely to see
|
||||||
|
it elsewhere. If you are modifying HTTP handling code, you should raise
|
||||||
|
this when you encounter an aborted request to make sure the socket is
|
||||||
|
closed cleanly.
|
||||||
|
|
||||||
|
``SynchronousOnlyOperation``
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. exception:: SynchronousOnlyOperation
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
|
||||||
|
The :exc:`SynchronousOnlyOperation` exception is raised when code that
|
||||||
|
is only allowed in synchronous Python code is called from an asynchronous
|
||||||
|
context (a thread with a running asynchronous event loop). These parts of
|
||||||
|
Django are generally heavily reliant on thread-safety to function and don't
|
||||||
|
work correctly under coroutines sharing the same thread.
|
||||||
|
|
||||||
|
If you are trying to call code that is synchronous-only from an
|
||||||
|
asynchronous thread, then create a synchronous thread and call it in that.
|
||||||
|
You can accomplish this is with ``asgiref.sync.sync_to_async``.
|
||||||
|
|
||||||
.. currentmodule:: django.urls
|
.. currentmodule:: django.urls
|
||||||
|
|
||||||
URL Resolver exceptions
|
URL Resolver exceptions
|
||||||
|
|
|
@ -44,6 +44,28 @@ MariaDB support
|
||||||
Django now officially supports `MariaDB <https://mariadb.org/>`_ 10.1 and
|
Django now officially supports `MariaDB <https://mariadb.org/>`_ 10.1 and
|
||||||
higher. See :ref:`MariaDB notes <mariadb-notes>` for more details.
|
higher. See :ref:`MariaDB notes <mariadb-notes>` for more details.
|
||||||
|
|
||||||
|
ASGI support
|
||||||
|
------------
|
||||||
|
|
||||||
|
Django 3.0 begins our journey to making Django fully async-capable by providing
|
||||||
|
support for running as an `ASGI <https://asgi.readthedocs.io/>`_ application.
|
||||||
|
|
||||||
|
This is in addition to our existing WSGI support. Django intends to support
|
||||||
|
both for the foreseeable future. Async features will only be available to
|
||||||
|
applications that run under ASGI, however.
|
||||||
|
|
||||||
|
There is no need to switch your applications over unless you want to start
|
||||||
|
experimenting with asynchronous code, but we have
|
||||||
|
:doc:`documentation on deploying with ASGI </howto/deployment/asgi/index>` if
|
||||||
|
you want to learn more.
|
||||||
|
|
||||||
|
Note that as a side-effect of this change, Django is now aware of asynchronous
|
||||||
|
event loops and will block you calling code marked as "async unsafe" - such as
|
||||||
|
ORM operations - from an asynchronous context. If you were using Django from
|
||||||
|
async code before, this may trigger if you were doing it incorrectly. If you
|
||||||
|
see a ``SynchronousOnlyOperation`` error, then closely examine your code and
|
||||||
|
move any database operations to be in a synchronous child thread.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ arctangent
|
||||||
arg
|
arg
|
||||||
args
|
args
|
||||||
assistive
|
assistive
|
||||||
|
async
|
||||||
atomicity
|
atomicity
|
||||||
attr
|
attr
|
||||||
auth
|
auth
|
||||||
|
@ -115,6 +116,7 @@ conf
|
||||||
config
|
config
|
||||||
contenttypes
|
contenttypes
|
||||||
contrib
|
contrib
|
||||||
|
coroutines
|
||||||
covariance
|
covariance
|
||||||
criticals
|
criticals
|
||||||
cron
|
cron
|
||||||
|
@ -133,6 +135,7 @@ customizations
|
||||||
Dahl
|
Dahl
|
||||||
Daly
|
Daly
|
||||||
Danga
|
Danga
|
||||||
|
Daphne
|
||||||
Darussalam
|
Darussalam
|
||||||
databrowse
|
databrowse
|
||||||
datafile
|
datafile
|
||||||
|
@ -750,6 +753,7 @@ utc
|
||||||
UTF
|
UTF
|
||||||
util
|
util
|
||||||
utils
|
utils
|
||||||
|
Uvicorn
|
||||||
uwsgi
|
uwsgi
|
||||||
uWSGI
|
uWSGI
|
||||||
validator
|
validator
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -83,7 +83,7 @@ setup(
|
||||||
entry_points={'console_scripts': [
|
entry_points={'console_scripts': [
|
||||||
'django-admin = django.core.management:execute_from_command_line',
|
'django-admin = django.core.management:execute_from_command_line',
|
||||||
]},
|
]},
|
||||||
install_requires=['pytz', 'sqlparse'],
|
install_requires=['pytz', 'sqlparse', 'asgiref'],
|
||||||
extras_require={
|
extras_require={
|
||||||
"bcrypt": ["bcrypt"],
|
"bcrypt": ["bcrypt"],
|
||||||
"argon2": ["argon2-cffi >= 16.1.0"],
|
"argon2": ["argon2-cffi >= 16.1.0"],
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from asgiref.testing import ApplicationCommunicator
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
from django.core.signals import request_started
|
||||||
|
from django.db import close_old_connections
|
||||||
|
from django.test import SimpleTestCase, override_settings
|
||||||
|
|
||||||
|
from .urls import test_filename
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ROOT_URLCONF='asgi.urls')
|
||||||
|
class ASGITest(SimpleTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
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):
|
||||||
|
request_started.connect(close_old_connections)
|
||||||
|
|
||||||
|
@async_to_sync
|
||||||
|
async def test_get_asgi_application(self):
|
||||||
|
"""
|
||||||
|
get_asgi_application() returns a functioning ASGI callable.
|
||||||
|
"""
|
||||||
|
application = get_asgi_application()
|
||||||
|
# Construct HTTP request.
|
||||||
|
communicator = ApplicationCommunicator(application, self._get_scope(path='/'))
|
||||||
|
await communicator.send_input({'type': 'http.request'})
|
||||||
|
# Read the response.
|
||||||
|
response_start = await communicator.receive_output()
|
||||||
|
self.assertEqual(response_start['type'], 'http.response.start')
|
||||||
|
self.assertEqual(response_start['status'], 200)
|
||||||
|
self.assertEqual(
|
||||||
|
set(response_start['headers']),
|
||||||
|
{
|
||||||
|
(b'Content-Length', b'12'),
|
||||||
|
(b'Content-Type', b'text/html; charset=utf-8'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response_body = await communicator.receive_output()
|
||||||
|
self.assertEqual(response_body['type'], 'http.response.body')
|
||||||
|
self.assertEqual(response_body['body'], b'Hello World!')
|
||||||
|
|
||||||
|
@async_to_sync
|
||||||
|
async def test_file_response(self):
|
||||||
|
"""
|
||||||
|
Makes sure that FileResponse works over ASGI.
|
||||||
|
"""
|
||||||
|
application = get_asgi_application()
|
||||||
|
# Construct HTTP request.
|
||||||
|
communicator = ApplicationCommunicator(application, self._get_scope(path='/file/'))
|
||||||
|
await communicator.send_input({'type': 'http.request'})
|
||||||
|
# Get the file content.
|
||||||
|
with open(test_filename, 'rb') as test_file:
|
||||||
|
test_file_contents = test_file.read()
|
||||||
|
# Read the response.
|
||||||
|
response_start = await communicator.receive_output()
|
||||||
|
self.assertEqual(response_start['type'], 'http.response.start')
|
||||||
|
self.assertEqual(response_start['status'], 200)
|
||||||
|
self.assertEqual(
|
||||||
|
set(response_start['headers']),
|
||||||
|
{
|
||||||
|
(b'Content-Length', str(len(test_file_contents)).encode('ascii')),
|
||||||
|
(b'Content-Type', b'text/plain' if sys.platform.startswith('win') else b'text/x-python'),
|
||||||
|
(b'Content-Disposition', b'inline; filename="urls.py"'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response_body = await communicator.receive_output()
|
||||||
|
self.assertEqual(response_body['type'], 'http.response.body')
|
||||||
|
self.assertEqual(response_body['body'], test_file_contents)
|
|
@ -0,0 +1,15 @@
|
||||||
|
from django.http import FileResponse, HttpResponse
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
|
def helloworld(request):
|
||||||
|
return HttpResponse('Hello World!')
|
||||||
|
|
||||||
|
|
||||||
|
test_filename = __file__
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', helloworld),
|
||||||
|
path('file/', lambda x: FileResponse(open(test_filename, 'rb'))),
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleModel(models.Model):
|
||||||
|
field = models.IntegerField()
|
|
@ -0,0 +1,36 @@
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
|
||||||
|
from django.core.exceptions import SynchronousOnlyOperation
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.utils.asyncio import async_unsafe
|
||||||
|
|
||||||
|
from .models import SimpleModel
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseConnectionTest(SimpleTestCase):
|
||||||
|
"""A database connection cannot be used in an async context."""
|
||||||
|
@async_to_sync
|
||||||
|
async def test_get_async_connection(self):
|
||||||
|
with self.assertRaises(SynchronousOnlyOperation):
|
||||||
|
list(SimpleModel.objects.all())
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncUnsafeTest(SimpleTestCase):
|
||||||
|
"""
|
||||||
|
async_unsafe decorator should work correctly and returns the correct
|
||||||
|
message.
|
||||||
|
"""
|
||||||
|
@async_unsafe
|
||||||
|
def dangerous_method(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@async_to_sync
|
||||||
|
async def test_async_unsafe(self):
|
||||||
|
# async_unsafe decorator catches bad access and returns the right
|
||||||
|
# message.
|
||||||
|
msg = (
|
||||||
|
'You cannot call this from an async context - use a thread or '
|
||||||
|
'sync_to_async.'
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(SynchronousOnlyOperation, msg):
|
||||||
|
self.dangerous_method()
|
|
@ -8,10 +8,9 @@ import tempfile
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import local
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import _thread
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
@ -289,7 +288,7 @@ class TranslationTests(SimpleTestCase):
|
||||||
|
|
||||||
@override_settings(LOCALE_PATHS=extended_locale_paths)
|
@override_settings(LOCALE_PATHS=extended_locale_paths)
|
||||||
def test_pgettext(self):
|
def test_pgettext(self):
|
||||||
trans_real._active = local()
|
trans_real._active = Local()
|
||||||
trans_real._translations = {}
|
trans_real._translations = {}
|
||||||
with translation.override('de'):
|
with translation.override('de'):
|
||||||
self.assertEqual(pgettext("unexisting", "May"), "May")
|
self.assertEqual(pgettext("unexisting", "May"), "May")
|
||||||
|
@ -310,7 +309,7 @@ class TranslationTests(SimpleTestCase):
|
||||||
Translating a string requiring no auto-escaping with gettext or pgettext
|
Translating a string requiring no auto-escaping with gettext or pgettext
|
||||||
shouldn't change the "safe" status.
|
shouldn't change the "safe" status.
|
||||||
"""
|
"""
|
||||||
trans_real._active = local()
|
trans_real._active = Local()
|
||||||
trans_real._translations = {}
|
trans_real._translations = {}
|
||||||
s1 = mark_safe('Password')
|
s1 = mark_safe('Password')
|
||||||
s2 = mark_safe('May')
|
s2 = mark_safe('May')
|
||||||
|
@ -1882,7 +1881,7 @@ class TranslationFileChangedTests(SimpleTestCase):
|
||||||
self.assertEqual(gettext_module._translations, {})
|
self.assertEqual(gettext_module._translations, {})
|
||||||
self.assertEqual(trans_real._translations, {})
|
self.assertEqual(trans_real._translations, {})
|
||||||
self.assertIsNone(trans_real._default)
|
self.assertIsNone(trans_real._default)
|
||||||
self.assertIsInstance(trans_real._active, _thread._local)
|
self.assertIsInstance(trans_real._active, Local)
|
||||||
|
|
||||||
|
|
||||||
class UtilsTests(SimpleTestCase):
|
class UtilsTests(SimpleTestCase):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
from threading import local
|
|
||||||
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
|
@ -278,7 +279,7 @@ class TranslationBlockTransTagTests(SimpleTestCase):
|
||||||
@override_settings(LOCALE_PATHS=extended_locale_paths)
|
@override_settings(LOCALE_PATHS=extended_locale_paths)
|
||||||
def test_template_tags_pgettext(self):
|
def test_template_tags_pgettext(self):
|
||||||
"""{% blocktrans %} takes message contexts into account (#14806)."""
|
"""{% blocktrans %} takes message contexts into account (#14806)."""
|
||||||
trans_real._active = local()
|
trans_real._active = Local()
|
||||||
trans_real._translations = {}
|
trans_real._translations = {}
|
||||||
with translation.override('de'):
|
with translation.override('de'):
|
||||||
# Nonexistent context
|
# Nonexistent context
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from threading import local
|
from asgiref.local import Local
|
||||||
|
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
from django.templatetags.l10n import LocalizeNode
|
from django.templatetags.l10n import LocalizeNode
|
||||||
|
@ -136,7 +136,7 @@ class TranslationTransTagTests(SimpleTestCase):
|
||||||
@override_settings(LOCALE_PATHS=extended_locale_paths)
|
@override_settings(LOCALE_PATHS=extended_locale_paths)
|
||||||
def test_template_tags_pgettext(self):
|
def test_template_tags_pgettext(self):
|
||||||
"""{% trans %} takes message contexts into account (#14806)."""
|
"""{% trans %} takes message contexts into account (#14806)."""
|
||||||
trans_real._active = local()
|
trans_real._active = Local()
|
||||||
trans_real._translations = {}
|
trans_real._translations = {}
|
||||||
with translation.override('de'):
|
with translation.override('de'):
|
||||||
# Nonexistent context...
|
# Nonexistent context...
|
||||||
|
|
Loading…
Reference in New Issue