blog Django Signals

Tags:
django

While experimenting with yamdl to prototype an idea for a wiki-like django site, I was curious about the various lower level Django signals. To better understand them, I wrote a simple app config to log the various checks. To help them be more visible in the terminal, I have also somewhat randomly added some assorted colors.

import os
from pathlib import Path

from django.apps import AppConfig
from django.core.checks import register
from django.db.backends.signals import connection_created
from django.db.models.signals import class_prepared
from django.dispatch import receiver
from django.utils.autoreload import BaseReloader, autoreload_started, file_changed
from django.utils.termcolors import make_style

red = make_style(fg="red")
blue = make_style(fg="blue")
cyan = make_style(fg="cyan")
yellow = make_style(fg="yellow")
magenta = make_style(fg="magenta")
bold_yellow = make_style(opts=("bold",), fg="yellow")


class TestappConfig(AppConfig):
    name = __package__

    def ready(self):
        print(bold_yellow("ready"), __class__, os.getpid())
        if os.environ.get("RUN_MAIN"):
            print(bold_yellow("On app thread"))


@register
def test_checks(*args, **kwargs):
    print(red("test_checks"), args, kwargs)
    return []


@receiver(connection_created, dispatch_uid="print_connection_created")
def print_connection_created(*args, **kwargs):
    print(red("connection_created"), args, kwargs)


@receiver(autoreload_started, dispatch_uid="print_autoreload_started")
def print_autoreload_started(sender: BaseReloader, **kwargs):
    print(magenta("autoreload_started"), sender, kwargs)


@receiver(file_changed, dispatch_uid="print_file_changed")
def print_file_changed(sender: BaseReloader, file_path: Path, **kwargs):
    print(magenta("file_changed"), sender, file_path, kwargs)


@receiver(class_prepared, dispatch_uid="print_class_prepared")
def print_class_prepared(sender, **kwargs):
    print(cyan("print_class_prepared"), sender, kwargs)

After adding my new app to my INSTALLED_APPS setting, I get a similar output when I run it.

uv run manage.py runserver
print_class_prepared <class 'django.contrib.contenttypes.models.ContentType'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
print_class_prepared <class 'django.contrib.admin.models.LogEntry'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
print_class_prepared <class 'django.contrib.auth.models.Permission'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
print_class_prepared <class 'django.contrib.auth.models.Group_permissions'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
print_class_prepared <class 'django.contrib.auth.models.Group'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
print_class_prepared <class 'django.contrib.auth.models.User_groups'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
print_class_prepared <class 'django.contrib.auth.models.User_user_permissions'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
print_class_prepared <class 'django.contrib.auth.models.User'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
print_class_prepared <class 'django.contrib.sessions.models.Session'> {'signal': <django.dispatch.dispatcher.Signal object at 0x10b7d8e30>}
ready <class 'sigtest.testapp.apps.TestappConfig'> 16393
print_class_prepared <class 'django.contrib.contenttypes.models.ContentType'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
print_class_prepared <class 'django.contrib.admin.models.LogEntry'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
print_class_prepared <class 'django.contrib.auth.models.Permission'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
print_class_prepared <class 'django.contrib.auth.models.Group_permissions'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
print_class_prepared <class 'django.contrib.auth.models.Group'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
print_class_prepared <class 'django.contrib.auth.models.User_groups'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
print_class_prepared <class 'django.contrib.auth.models.User_user_permissions'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
print_class_prepared <class 'django.contrib.auth.models.User'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
print_class_prepared <class 'django.contrib.sessions.models.Session'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
ready <class 'sigtest.testapp.apps.TestappConfig'> 16395
On app thread
Watching for file changes with StatReloader
Performing system checks...

test_checks () {'app_configs': None, 'databases': None}
autoreload_started <django.utils.autoreload.StatReloader object at 0x1034e14f0> {'signal': <django.dispatch.dispatcher.Signal object at 0x1028081d0>}
System check identified no issues (0 silenced).
connection_created () {'signal': <django.dispatch.dispatcher.Signal object at 0x1031824b0>, 'sender': <class 'django.db.backends.sqlite3.base.DatabaseWrapper'>, 'connection': <DatabaseWrapper vendor='sqlite' alias='default'>}
print_class_prepared <class 'django.db.migrations.recorder.MigrationRecorder.Migration.<locals>.Migration'> {'signal': <django.dispatch.dispatcher.Signal object at 0x102a7a540>}
December 24, 2024 - 10:23:57
Django version 5.1.4, using settings 'sigtest.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

file_changed <django.utils.autoreload.StatReloader object at 0x1015447a0> **/sigtest/testapp/apps.py {'signal': <django.dispatch.dispatcher.Signal object at 0x100df15b0>}
**/sigtest/testapp/apps.py changed, reloading.

It is interesting that class_prepared and our app config’s ready fires twice. When using runserver it loads the application, and then starts a thread for processing requests. In the now running thread, it will then run system checks before triggering the autoreload_started signal.

It is also interesting that this point it will start connecting to the database and trigger class_prepared on the default Migration class.

After changing a python file for a test, can also see where file_changed fired.

I want to attempt my own interpretation of something like yamdl so it seems like autoreload_started and file_changed are the signals I will need to consider.

It does not seem to be documented, but from reading the Django code, returning True from a file_changed handler prevents restarting the watch server, while anything else tells Django to restart the worker. This will also be handy for what I’m working on.

Edit 2025-01-02: I have added a sample app to https://github.com/kfdm/django_debug_signals and uploaded to https://pypi.org/project/django-debug-signals/ to save others a bit of typing.