As part of my Markdown Virtual Table project, when running the development runserver , I occasionally want to run some code when we update a file.
There are two signals that we can use for this, that are not well documented in Django itself:
from django.utils.autoreload import autoreload_started, file_changed
These are mostly used internally with the development runserver.
autoreload_started
The first half of this is used to register our file watches. We can find some of these examples in Django itself.
In the Django translation system, we find this watcher that ultimately looks for all **/*.mo
translation files.
https://github.com/django/django/blob/5.2/django/utils/translation/reloader.py#L9-L22
In the Django template system, we find this watcher that looks for **/*
for each template directory
https://github.com/django/django/blob/5.2/django/template/autoreload.py#L48-L51
In our own app, we can use code like this to watch for any changes to markdown files.
from django.dispatch import receiver
from django.utils.autoreload import autoreload_started, BaseReloader
@receiver(autoreload_started)
def register_watches(sender: BaseReloader, **kwargs):
sender.watch_dir(settings.BASE_DIR, "**/*.md")
sender.watch_dir(settings.BASE_DIR, "**/*.markdown")
file_changed
With our watch signals configured, we then want to register a file_changed
signal.
In the Django translation system, we have a translation_file_changed
that matches our above watch.
Any time the changed path ends with .mo
Django will handle the translation cache.
https://github.com/django/django/blob/5.2/django/utils/translation/reloader.py#L25-L36
In the Django template system, we exit early if we find a file with a .py
extension, otherwise, we reset the template loaders, similar to how we reset the translation cache.
https://github.com/django/django/blob/5.2/django/template/autoreload.py#L54-L61
In a similar way, we can register our file change signal to do something on update to files.
from django.dispatch import receiver
from django.utils.autoreload import file_changed, BaseReloader
@receiver(file_changed)
def process_file_changed(file_path: Path, **kwargs):
if file_path.suffix in ['.md', '.markdown']:
# Just log debug information for now
print(file_path, kwargs)
The most confusing part of the file_changed
signal, is what value we need to return.
Since it is not well documented, we need to look at the code.
https://github.com/django/django/blob/5.2/django/utils/autoreload.py#L368-L372
# Annotated version of the above
def notify_file_changed(self, path):
# Whenever a file is changed, send the path of the changed file to each
# registered signal. We will get the result. For the results, we will get
# a list of tuples with the receiver and return code
# [ (receiver_method, result), (receiver_method, result) ]
results = file_changed.send(sender=self, file_path=path)
# We then check any() on *just* the results, which could look something like
# any([True, False, None])
# So ultimately, if any of our file_changed receivers, return True, then we
# will *not* trigger a restart of our runserver
if not any(res[1] for res in results):
trigger_reload(path)
Since we likely do not want to restart on changes to markdown files in our case, we can update our above receiver to return True when we handle our files.
from django.dispatch import receiver
from django.utils.autoreload import file_changed, BaseReloader
@receiver(file_changed)
def process_file_changed(file_path: Path, **kwargs):
if file_path.suffix in ['.md', '.markdown']:
# Just log debug information for now
print(file_path, kwargs)
# Since we've handled our markdown files, we return True as to *not*
# trigger a runserver restart
return True
# But if we fall through, our method returns None which will yield to
# other file_changed handlers that could potentially restart the server
So for our file_changed
receivers, we can interpret returning True
as a file_changed event being handled without requiring a runserver restart.
Bonus
If you’re working with file watchers, running with pywatchman can result in more efficent running.
# Install the watchman daemon to run in the background
brew install watchman
# install pywatchman to our Django project in dev mode
uv add --dev pywatchman
See also Efficient Reloading