Django 5 signal asend: unhashable type list

74 Views Asked by At

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.

1

There are 1 best solutions below

1
illagrenan On BEST ANSWER

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)]

Update 1

Bugreport in Django project has been accepted, I prepared a PR: https://github.com/django/django/pull/17837.

Update 2

Fix should be released in Django 5.0.3 (https://docs.djangoproject.com/en/5.0/releases/5.0.3/).