Trying to make a short example with django 5 async signal. Here is the code:
View:
async def confirm_email_async(request, code):
await user_registered_async.asend(
sender=User,
)
return JsonResponse({"status": "ok"})
Signal:
user_registered_async = Signal()
@receiver(user_registered_async)
async def async_send_welcome_email(sender, **kwargs):
print("Sending welcome email...")
await asyncio.sleep(5)
print("Email sent")
The error trace is:
Traceback (most recent call last):
File "C:\Users\karonator\AppData\Roaming\Python\Python311\site-packages\asgiref\sync.py", line 534, in thread_handler
raise exc_info[1]
File "C:\Users\karonator\AppData\Roaming\Python\Python311\site-packages\django\core\handlers\exception.py", line 42, in inner
response = await get_response(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\karonator\AppData\Roaming\Python\Python311\site-packages\asgiref\sync.py", line 534, in thread_handler
raise exc_info[1]
File "C:\Users\karonator\AppData\Roaming\Python\Python311\site-packages\django\core\handlers\base.py", line 253, in _get_response_async
response = await wrapped_callback(
^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\karonator\Desktop\signals\main\views.py", line 34, in confirm_email_async
await user_registered_async.asend(
File "C:\Users\karonator\AppData\Roaming\Python\Python311\site-packages\django\dispatch\dispatcher.py", line 250, in asend
responses, async_responses = await asyncio.gather(
^^^^^^^^^^^^^^^
File "C:\Program Files\Python311\Lib\asyncio\tasks.py", line 819, in gather
if arg not in arg_to_fut:
^^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'list'
Will be grateful for any help, already broken my head. Thanks for your time.
In my opinion this is a bug in Django 5. I came across the same problem and created a report: https://code.djangoproject.com/ticket/35174. The asend
method (as well as asend_robust
) crashes if the signal does not have at least one synchronous receiver.
Here is a complete example of how you can use async in your project before the official fix is available (if it is indeed a bug):
"""
To run this file, save it as `signals.py` and use the following command:
$ uvicorn signals:app --log-level=DEBUG --reload
$ curl -v http://127.0.0.1:8000
"""
import asyncio
import logging
from asgiref.sync import sync_to_async
from django import conf, http, urls
from django.core.handlers.asgi import ASGIHandler
from django.dispatch import Signal, receiver
from django.dispatch.dispatcher import NO_RECEIVERS
logging.basicConfig(level=logging.DEBUG)
conf.settings.configure(
ALLOWED_HOSTS="*",
ROOT_URLCONF=__name__,
LOGGING=None,
)
app = ASGIHandler()
class PatchedSignal(Signal):
async def asend(self, sender, **named):
"""
For details see this: https://code.djangoproject.com/ticket/35174
"""
if (
not self.receivers
or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
):
return []
sync_receivers, async_receivers = self._live_receivers(sender)
if sync_receivers:
@sync_to_async
def sync_send():
responses = []
for receiver in sync_receivers:
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
return responses
else:
# >>>>>>
# THIS IS THE PATCHED PART:
async def sync_send():
return []
# <<<<<<
responses, async_responses = await asyncio.gather(
sync_send(),
asyncio.gather(
*(
receiver(signal=self, sender=sender, **named)
for receiver in async_receivers
)
),
)
responses.extend(zip(async_receivers, async_responses))
return responses
user_registered_async = PatchedSignal()
@receiver(user_registered_async)
async def async_send_welcome_email(sender, **kwargs):
logging.info("async_send_welcome_email::started")
await asyncio.sleep(1)
logging.info("async_send_welcome_email::finished")
async def root(request):
logging.info("root::started")
await user_registered_async.asend(sender=None)
logging.info("root::ended")
return http.JsonResponse({"message": "Hello World"})
urlpatterns = [urls.path("", root)]
Bugreport in Django project has been accepted, I prepared a PR: https://github.com/django/django/pull/17837.
Fix should be released in Django 5.0.3 (https://docs.djangoproject.com/en/5.0/releases/5.0.3/).