pythondjangopython-asynciodjango-signals

Django 5 signal asend: unhashable type list


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.


Solution

  • 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/).