From c90ab30fa1305481024b9c3c50b5a6ed6cd9a2f5 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Mon, 2 Dec 2019 13:02:21 -0700 Subject: [PATCH] Fixed #31056 -- Allowed disabling async-unsafe check with an environment variable. --- django/utils/asyncio.py | 18 ++++++++++-------- docs/releases/3.0.1.txt | 5 ++++- docs/spelling_wordlist | 1 + docs/topics/async.txt | 20 ++++++++++++++++++++ tests/async/tests.py | 13 ++++++++++++- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/django/utils/asyncio.py b/django/utils/asyncio.py index c4de04ba127..2405e3413e1 100644 --- a/django/utils/asyncio.py +++ b/django/utils/asyncio.py @@ -1,5 +1,6 @@ import asyncio import functools +import os from django.core.exceptions import SynchronousOnlyOperation @@ -12,14 +13,15 @@ def async_unsafe(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) + if not os.environ.get('DJANGO_ALLOW_ASYNC_UNSAFE'): + # 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 diff --git a/docs/releases/3.0.1.txt b/docs/releases/3.0.1.txt index 18e2ea44c4a..589ef40499d 100644 --- a/docs/releases/3.0.1.txt +++ b/docs/releases/3.0.1.txt @@ -9,4 +9,7 @@ Django 3.0.1 fixes several bugs in 3.0. Bugfixes ======== -* ... +* Fixed a regression in Django 3.0 by restoring the ability to use Django + inside Jupyter and other environments that force an async context, by adding + and option to disable :ref:`async-safety` mechanism with + ``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable (:ticket:`31056`). diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 9c56339cc0f..6a36a117073 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -309,6 +309,7 @@ ize JavaScript Jinja jQuery +Jupyter jython Kaplan Kessler diff --git a/docs/topics/async.txt b/docs/topics/async.txt index 6a6f0030c9b..b341084b1ab 100644 --- a/docs/topics/async.txt +++ b/docs/topics/async.txt @@ -12,6 +12,8 @@ There is limited support for other parts of the async ecosystem; namely, Django can natively talk :doc:`ASGI `, and some async safety support. +.. _async-safety: + Async-safety ------------ @@ -34,3 +36,21 @@ code from an async context; instead, write your code that talks to async-unsafe in its own, synchronous function, and call that using ``asgiref.sync.async_to_sync``, or any other preferred way of running synchronous code in its own thread. + +If you are *absolutely* in dire need to run this code from an asynchronous +context - for example, it is being forced on you by an external environment, +and you are sure there is no chance of it being run concurrently (e.g. you are +in a Jupyter_ notebook), then you can disable the warning with the +``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable. + +.. warning:: + + If you enable this option and there is concurrent access to the + async-unsafe parts of Django, you may suffer data loss or corruption. Be + very careful and do not use this in production environments. + +If you need to do this from within Python, do that with ``os.environ``:: + + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + +.. _Jupyter: https://jupyter.org/ diff --git a/tests/async/tests.py b/tests/async/tests.py index 450a38018da..f42e5490755 100644 --- a/tests/async/tests.py +++ b/tests/async/tests.py @@ -1,5 +1,6 @@ +import os import sys -from unittest import skipIf +from unittest import mock, skipIf from asgiref.sync import async_to_sync @@ -39,3 +40,13 @@ class AsyncUnsafeTest(SimpleTestCase): ) with self.assertRaisesMessage(SynchronousOnlyOperation, msg): self.dangerous_method() + + @mock.patch.dict(os.environ, {'DJANGO_ALLOW_ASYNC_UNSAFE': 'true'}) + @async_to_sync + async def test_async_unsafe_suppressed(self): + # Decorator doesn't trigger check when the environment variable to + # suppress it is set. + try: + self.dangerous_method() + except SynchronousOnlyOperation: + self.fail('SynchronousOnlyOperation should not be raised.')