Signals
When (and when not) to use them
David Seddon
david@seddonym.me
http://seddonym.me
- Know what they are?
- Use them?
- Like them?
- Find them annoying?
Do you...
Django signals
- What are signals for?
- Under the hood
- Testing: tips and pitfalls
This talk
Part 1
What are signals for?
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)
Receiver decorator
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
Including receivers
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'
Additional kwargs
@receiver(signal)
def my_receiver(sender, foo, **kwargs):
pass
my_signal = Signal(providing_args=['foo'])
my_signal.send(sender=None, foo='bar')
Sender
@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
But why?
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.
Built in signals
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/
Part 2
Under the hood
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
Dispatcher API
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.
Weak references
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.
Order of calls
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.
Exception handling
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
Part 3
Testing:
tips and pitfalls
Two things to test
- Connection - whether or not the receiver will be called.
- Behaviour - The internal behaviour of the receiver once called.
Testing gotchas
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?
Testing gotchas
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:
Testing receivers without import
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
What happens when we add behaviour testing?
Testing behaviour
Testing connection
OR
If any of your tests import or mock anything in your receivers module, this will undermine your connection test.
Test execution order
So...
- django.test.TestCase
- All other Django-based tests
- unittest.TestCase
My best answer
django.test.TestCase
- Receiver connection tests
- Other tests that hit the database
- Nothing that imports or mocks receivers
unittest.TestCase
- Mocky unit tests for receiver behaviour
- Other tests that don't hit the database
Nowhere in any test should anything from receivers file be imported, except within a test method context, or in a mock patch.
Main take aways
-
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
Signals
By David Seddon
Signals
When (and when not) to use them
- 2,254