From a415ce70bef6d91036b00dd2c8544aed7aeeaaed Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 12 Apr 2019 06:15:18 -0700 Subject: 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. --- django/utils/asyncio.py | 32 ++++++++++++++++++++++++++++++++ django/utils/timezone.py | 4 ++-- django/utils/translation/reloader.py | 5 +++-- django/utils/translation/trans_real.py | 5 +++-- 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 django/utils/asyncio.py (limited to 'django/utils') diff --git a/django/utils/asyncio.py b/django/utils/asyncio.py new file mode 100644 index 0000000000..c4de04ba12 --- /dev/null +++ b/django/utils/asyncio.py @@ -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 diff --git a/django/utils/timezone.py b/django/utils/timezone.py index 58e92c1fa8..4c43377447 100644 --- a/django/utils/timezone.py +++ b/django/utils/timezone.py @@ -6,9 +6,9 @@ import functools import warnings from contextlib import ContextDecorator from datetime import datetime, timedelta, timezone, tzinfo -from threading import local import pytz +from asgiref.local import Local from django.conf import settings from django.utils.deprecation import RemovedInDjango31Warning @@ -89,7 +89,7 @@ def get_default_timezone_name(): return _get_timezone_name(get_default_timezone()) -_active = local() +_active = Local() def get_current_timezone(): diff --git a/django/utils/translation/reloader.py b/django/utils/translation/reloader.py index 8e2d320208..2d69ad44e0 100644 --- a/django/utils/translation/reloader.py +++ b/django/utils/translation/reloader.py @@ -1,6 +1,7 @@ -import threading from pathlib import Path +from asgiref.local import Local + from django.apps import apps @@ -25,5 +26,5 @@ def translation_file_changed(sender, file_path, **kwargs): gettext._translations = {} trans_real._translations = {} trans_real._default = None - trans_real._active = threading.local() + trans_real._active = Local() return True diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index f4985fb3c1..e089597ccb 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -5,7 +5,8 @@ import os import re import sys import warnings -from threading import local + +from asgiref.local import Local from django.apps import apps from django.conf import settings @@ -20,7 +21,7 @@ from . import to_language, to_locale # Translations are cached in a dictionary for every language. # The active translations are stored by threadid to make them thread local. _translations = {} -_active = local() +_active = Local() # The default translation is based on the settings file. _default = None -- cgit v1.3