[Django] #34737: Python 3.10 compatibility changes to utils/asyncio.py raise SynchronousOnlyOperation for non-running event loops on python >= 3.7

17 views
Skip to first unread message

Django

unread,
Jul 24, 2023, 9:14:30 AM7/24/23
to django-...@googlegroups.com
#34737: Python 3.10 compatibility changes to utils/asyncio.py raise
SynchronousOnlyOperation for non-running event loops on python >= 3.7
---------------------------------------+------------------------
Reporter: wbastian-bh | Owner: nobody
Type: Bug | Status: new
Component: Utilities | Version: 3.2
Severity: Normal | Keywords:
Triage Stage: Unreviewed | Has patch: 0
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
---------------------------------------+------------------------
**Env**
Django 3.2.9+
Python 3.7+

**Overview**
With this commit
(https://github.com/django/django/commit/53fad80ffe16ab4edb713b1ef0090d0fcf63565a),
which was included with the 3.2.9 release if we're on PY3.7+, we raise
SynchronousOnlyOperation when asyncio.get_running_loop returns an object
without checking event_loop.is_running().

It appears that asyncio.get_running_loop can return non-running loops, as
observed by including a logging statement before raising the
SynchronousOnlyOperation.

If my understanding is correct, get_running_loop should only be returning
running loops, and is not.

Curious if we can continue to leverage event_loop.is_running() in all
cases.

**Observation Example**
{{{
import asyncio
import functools
import os

from django.core.exceptions import SynchronousOnlyOperation
from django.utils.version import PY37


if PY37:
get_running_loop = asyncio.get_running_loop
else:
get_running_loop = asyncio.get_event_loop


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):
if not os.environ.get('DJANGO_ALLOW_ASYNC_UNSAFE'):
# Detect a running event loop in this thread.
try:
event_loop = get_running_loop()
except RuntimeError:
pass
else:
if PY37 or event_loop.is_running():
print(f"raising SynchronousOnlyOperation on
{event_loop} where is_running = {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
}}}

**Observation Output**
{{{
raising SynchronousOnlyOperation on <_UnixSelectorEventLoop running=False
closed=False debug=False> where is_running = False
}}}

**Steps to reproduce**
1. Have a non-running event loop present and do just about anything in
Django
2. See SynchronousOnlyOperation raised

--
Ticket URL: <https://code.djangoproject.com/ticket/34737>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Reply all
Reply to author
Forward
0 new messages