When (and when not) to use them
David Seddon
david@seddonym.me
http://seddonym.me
Do you...
Django signals
This talk
signal
receiver
receiver
receiver
receiver
send()
A signal sends messages to a set of connected receivers.
signal
receiver
send()
from django.dispatch import Signal
my_signal = Signal()
def my_receiver(sender, **kwargs):
pass
connect
my_signal.connect(my_receiver)
my_signal.send(sender=None)
from django.dispatch import receiver
@receiver(signal)
def my_receiver(sender, **kwargs):
pass
def my_receiver(sender, **kwargs):
pass
my_signal.connect(my_receiver)
Instead of
use
You can define your receivers anywhere, but you need to make sure they get imported.
My preferred approach:
Define receivers in receivers.py in the relevant app.
In one of your apps, use module autodiscovery to automatically import receivers.py files.
# main/__init__.py
from django.apps import AppConfig as BaseAppConfig
from django.utils.module_loading import autodiscover_modules
class AppConfig(BaseAppConfig):
name = 'main'
def ready(self):
# Automatically import all receivers files
autodiscover_modules('receivers')
default_app_config = 'main.AppConfig'
@receiver(signal)
def my_receiver(sender, foo, **kwargs):
pass
my_signal = Signal(providing_args=['foo'])
my_signal.send(sender=None, foo='bar')
@receiver(signal, sender=MyClass)
def my_receiver(sender, **kwargs):
# This will only get called if MyClass is passed as the
# sender when the signal is sent.
pass
class MyClass:
pass
my_signal.send(sender=None)
my_signal.send(sender='foo')
my_signal.send(sender=MyClass)
Use sender to connect your receivers only when the signal is sent from a specific sender
Django includes a “signal dispatcher” which helps allow decoupled applications get notified when actions occur elsewhere in the framework.
The Django docs
App
App
App
App
App
App
Django project with lots of circular dependencies (i.e. coupled applications)
Settings
App
App
App
App
App
App
Encapsulated Django project
(all the arrows go in one direction)
Settings
App
App
Signals allow apps higher up the dependency chain to react to things lower down.
receiver
signal
The only reason to use signals
say_something('hello')
def say_something(message):
Going with the flow:
no need for signals
Going against the flow:
use signals
App
App
Signals allow apps higher up the dependency chain to react to things lower down.
receiver
signal
The only reason to use signals
say_something('hello')
def say_something(message):
Going with the flow:
no need for signals
Going against the flow:
use signals
App
App
Signals allow apps higher up the dependency chain to react to things lower down.
receiver
signal
The only reason to use signals
say_something('hello')
def say_something(message):
Going with the flow:
no need for signals
Going against the flow:
use signals
The only reason for implementing a signal/receiver call is if it would break a dependency chain.
By far the most common use of signals is to react to Django's built in signals.
Text
from django.db.models.signals import post_save
from django.contrib.auth import User
from .utils import send_welcome_email
@receiver(post_save, sender=User)
def user_post_save_creation(sender, instance, created, **kwargs):
if not created:
return
# Send a welcome email to new users
send_welcome_email(user=instance)
https://docs.djangoproject.com/en/1.10/ref/signals/
django/ dispatch/ __init__.py license.txt dispatcher.py weakref_backports.py
Imports dispatcher
PyDispatcher license
Main logic (316 lines)
Pre Python 3.4 Compatibility
Dispatcher framework is small
class Signal:
receivers
connect()
disconnect()
send()
receiver
Decorator that calls connect on the signal with the decorated function.
List of callbacks (well almost)
Adds supplied callback to the list
Removes supplied callback from the list
Calls each callback in receivers with the supplied arguments.
Signals store receiver callbacks by weak references, which means that they won't be held in memory if they're deleted elsewhere.
If that's a problem, you can pass weak=False when connecting:
def connect_local_receivers():
# Both of these inner functions will be garbage collected, by default.
def local_receiver_weak(sender, **kwargs):
print("Foo.")
def local_receiver_strong(sender, **kwargs):
print('Bar.')
my_signal.connect(local_receiver_weak)
my_signal.connect(local_receiver_strong, weak=False)
connect_local_receivers()
>>> my_signal.send(sender=None)
Bar.
Receivers are called in the order in which they were connected.
Depending on how you connected the receivers, this will probably be the same order as they are listed in INSTALLED_APPS.
By default, any exceptions will be unhandled by the dispatcher.
>>> my_signal.send(sender=None, instance=instance)
Exception: Something bad happened in a receiver connected to this.
If you want to process all of the receivers in turn, handling each exception, use send_robust:
>>> results = my_signal.send_robust(sender=None, instance=instance)
>>> any([isinstance(e, Exception) for receiver, e in results])
True
from unittest import TestCase
from unittest.mock import patch
from apples.models import Apple
from apples.signals import apple_eaten
class TestReceivers(TestCase):
def test_receiver_connection(self):
"""Tests that my_receiver is connected to the apple_eaten signal."""
apple = Apple()
with patch('myapp.receivers.my_receiver') as mock_receiver:
apple_eaten.send(Apple, instance=apple)
mock_receiver.assert_called_once_with(sender=None, instance=apple)
What's the problem here?
from myapp.receivers import my_receiver
Be very careful with imports when testing connection of receivers.
Any imports or mocking of anything in the receivers module will automatically connect your receiver.
with patch('myapp.receivers.my_receiver') as mock_receiver:
from django.test import TestCase, skip
from apples.models import Apple
from apples.signals import apple_eaten
from django.db.models import signals
class ReceiverConnectionTestCase(TestCase):
"""TestCase that allows asserting that a given receiver is connected to a signal.
"""
def assert_receiver_is_connected(self, receiver_string, signal, sender):
receivers = signal._live_receivers(sender)
receiver_strings = ["{}.{}".format(r.__module__, r.__name__) for r in receivers]
if receiver_string not in receiver_strings:
raise AssertionError('{} is not connected to signal.'.format(receiver_string))
class TestConnection(ReceiverConnectionTestCase):
def test_receiver_is_connected(self):
self.assert_receiver_is_connected('myapp.receivers.my_receiver', apple_eaten, Apple)
Gist: http://bit.ly/2mmaWeS
Testing behaviour
Testing connection
OR
If any of your tests import or mock anything in your receivers module, this will undermine your connection test.
So...
django.test.TestCase
unittest.TestCase
Nowhere in any test should anything from receivers file be imported, except within a test method context, or in a mock patch.
Only use signals to avoid introducing circular dependencies.
Signals in signals.py,
receivers in receivers.py.
Beware when importing or mocking receivers in your tests.
This talk's slides:
slides.com/davidseddon/signals
We're hiring!
www.growthstreet.co.uk/careers
Encapsulated Django talk:
http://bit.ly/2mrH9hZ
David Seddon
david@seddonym.me
seddonym.me