Showing posts with label signals. Show all posts
Showing posts with label signals. Show all posts

Wednesday, 16 January 2013

Signals - Part 2: Return to sender

This week we return with a follow up to one of the most popular topics on this blog - signals. In my first post on the topic (Signals: Letting People Know When You've Got Something To Say) I covered the basics of how to register a function to listen to signals as well as how to create your own signals and use them to send messages. Since that post there has been a new decorator added to help register your listeners which I will talk about more shortly. The other topics I will be covering are some of the other options that can be passed in when registering a signal and the mechanism provided for obtaining responses from listeners when a signal is sent.

The Receiver Decorator

This decorator was added in django 1.3 and provides a shortcut to having to call connect for your listener functions. It's very simple to use and takes the same parameters that connect does (behind the scenes it passes these straight through to the connect function for you) as shown in my first example (modified from example 1 of the first signals post.

# File: test_signal/models.py from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver class UserProfile(models.Model): user = models.ForeignKey(User, unique=True) allow_our_emails = models.BooleanField(default=False) allow_other_emails = models.BooleanField(default=False) @receiver(post_save, sender=User) def ensure_profile_exists(sender, **kwargs): if kwargs.get('created', False): UserProfile.objects.create(user=kwargs.get('instance')) post_save.connect(ensure_profile_exists, sender=User)

The second example shows a change from Django 1.5 to the receiver decorator which allows you to assign a function to listen to multiple signals in one go.

# File: test_signal/models.py from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver class UserProfile(models.Model): user = models.ForeignKey(User, unique=True) allow_our_emails = models.BooleanField(default=False) allow_other_emails = models.BooleanField(default=False) @receiver([post_save,pre_delete,], sender=User) def ensure_profile_exists(sender, **kwargs): if kwargs.get('created', False): UserProfile.objects.create(user=kwargs.get('instance')) signal = kwargs.get('signal', None) if signal == post_save: # After saving ensure that a UserProfile exists. if kwargs.get('created', False): UserProfile.objects.create(user=kwargs.get('instance')) elif signal == pre_delete: # Before deleting remove the UserProfile UserProfile.objects.filter(user=kwargs.get('instance')).delete()

Other Connection Arguments

So far I've only mentioned the sender argument that can be provided to connect or receiver there are a couple of other arguments that may be useful from time to time.

dispatch_uid
This allows you to pass in a unique identifier for your listener. If the signal you're trying to connect to already has a listener with that id then the new one won't be added. You can use this for situations where the code connecting receiver may end up running more than once, or for situations where different parts of the code may try to add a listener for a specific purpose but you only need one of them to actually receive the signals.
weak
This is True by default and refers to the way in which the signal stores the listener. If the value is True then a weak reference to the listening function is stored so if the function drops out of scope then it will be forgotten about and won't receive signals any more. If the value is False however you can pass in local functions without worrying that they'll be garbage collected.

Example 3 shows a situation where these parameters may be useful

# File: test_signal/models.py from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver class LoggableModel1(models.Model): value = models.TextField() class LoggableModel2(models.Model): val = models.TextField() def add_logging_signals(models): for model_inst in models: uid = "%s-logger" % str(model_inst) if hasattr(model_inst, 'val') @receiver(post_save, sender=model_inst, weak=False, dispatch_uid=uid) def _listener(sender, kwargs**): print kwargs['instance'].val elif hasattr(model_inst, 'value') @receiver(post_save, sender=model_inst, weak=False, dispatch_uid=uid) def _listener(sender, kwargs**): print kwargs['instance'].value add_logging_signals([LoggableModel1, LoggableModel2, LoggableModel1,])

In this example because the listener functions are being created on the fly when add_logging_signals is called we specify weak=False so that the signal keeps hold of the function even when it drops out of scope.

We also make use of dispatch_uid so that even if we accidentally pass a model into the signal creator twice (which we then did) it will only add the one listener and so only output the logging code once.

Responding to a signal

The final topic for this week is on responding to signals. Signals don't have to be just a one-way mechanism!

The next example shows this in action

# File: signals/signals.py from django.dispatch import Signal pizza_deliverer = Signal(providing_args=["topping","size",])
# File: signals/models.py from django.db import models from django.dispatch import receiver from django.db.models.signals import post_save, class_prepared from signals import pizza_deliverer # List of toppings that our pizzas can have: TOPPING_PLAIN = 0 TOPPING_PEPPERONI = 1 TOPPING_VEGETARIAN = 2 TOPPINGS = ((TOPPING_PLAIN, 'Plain',), (TOPPING_PEPPERONI, 'Pepperoni',), (TOPPING_VEGETARIAN, 'Vegetarian',), ) # List of sizes that the pizzas could be: SIZE_SMALL = 8 SIZE_MEDIUM = 10 SIZE_LARGE = 12 SIZES = ((SIZE_SMALL, 'Small',), (SIZE_MEDIUM, 'Medium',), (SIZE_LARGE, 'Large',), ) class Pizza(models.Model): topping = models.IntegerField(choices=TOPPINGS) size = models.IntegerField(choices=SIZES) customer = models.ForeignKey('Customer', null=True, blank=True, default=None) @property def uid(self): return "pizza-%s" % self.id def serve_pizza(self, customer): # We have a customer so stop listening for new ones. self.customer = customer self.save() customer.receive_pizza() pizza_deliverer.disconnect(dispatch_uid=self.uid, sender=Customer, weak=False) class Customer(models.Model): topping_wanted = models.IntegerField(choices=TOPPINGS) size_wanted = models.IntegerField(choices=SIZES) @property def uid(self): return "customer-%s" % self.id def receive_pizza(self): # We have our pizza so stop listening for new ones. pizza_deliverer.disconnect(dispatch_uid=self.uid, sender=Pizza, weak=False) def _process_pizza(pizza): hungry_customers = pizza_deliverer.send(sender=Pizza, topping=pizza.topping, size=pizza.size) for _receiver, customer in hungry_customers: if customer is not None: # Pizza goes to the first customer that wants it. pizza.serve_pizza(customer) break else: # Nobody wants this pizza yet so listen for someone that does. @receiver(pizza_deliverer, sender=Customer, weak=False, dispatch_uid=pizza.uid) def _wait_for_customer(sender, **kwargs): topping = kwargs.get('topping') size = kwargs.get('size') if topping == pizza.topping and size == pizza.size: return pizza @receiver(post_save, sender=Pizza) def new_pizza(sender, **kwargs): if kwargs.get('created', False): pizza = kwargs.get('instance') _process_pizza(pizza) def _process_customer(customer): prepared_pizzas = pizza_deliverer.send(sender=Customer, topping=customer.topping_wanted, size=customer.size_wanted) for _receiver, pizza in prepared_pizzas: if pizza is not None: # Customer takes the first pizza offered that matches their criteria. pizza.serve_pizza(customer) break else: # The pizza this customer wants isn't ready yet so wait for it. @receiver(pizza_deliverer, sender=Pizza, weak=False, dispatch_uid=customer.uid) def _wait_for_pizza(sender, **kwargs): topping = kwargs.get('topping') size = kwargs.get('size') if customer.topping_wanted == topping and customer.size_wanted == size: return customer @receiver(post_save, sender=Customer) def new_customer(sender, **kwargs): if kwargs.get('created', False): customer = kwargs.get('instance') _process_customer(customer) # Ensure listeners are added for any unallocated pizzas or customers. for pizza in Pizza.objects.filter(customer=None): _process_pizza(pizza) for customer in Customer.objects.filter(pizza=None): _process_customer(customer)

Now this rather convoluted example simulates a potential (inneficient) pizzaria and makes use of all the features of signals discussed so far.

The signal is created as before and is set up to take two parameters a topping and a size. This signal represents the person behind the counter who will look for a waiting customer when a new pizza arrives or look for a waiting pizza when there's a new customer.

When a new pizza is added, a call is sent out to all the listening customers with the topping and size. Received responses are then iterated through until a positive reply is found, at which point the pizza is allocated to that customer.

If no suitable customers are listening then the pizza registers a listener function with our deliverer signal so it can wait for an appropriate customer to arrive.

A similar setup is then created for the customers - new ones send out a signal looking for matching pizzas and if there's none they sit and wait.

The listening functions _wait_for_customer and _wait_for_pizza both require a strong reference as they're created on the fly as well as both utilising the dispatch_uid to ensure that there's only one listener per pizza or customer.

This code also make use of the disconnect method on a signal to ensure that once a pizza and customer are matched up neither of them listens out for any further messages.

The final mention on signals is for the send_robust this would be used the same way that we used send in example 4 however it will catch any exceptions thrown by the listening functions and add that as the response from that listener.

Therefore to use it in our example we would also need to change the check when we iterate through the responses to ensure that the response was not None and that the response was also not of type Exception

This concludes the post on advanced signalling in Django. Until next time - keep up the communicating!

Sunday, 17 May 2009

Signals: Letting People Know When You've Got Something To Say

This week marks a break away from the topic of templates and instead will be focussing on signals.


Signals are Django's way of allowing you to listen out for certain events that either occur by default in the Django code or that have been specified by an application developer. The signals provided by default are explained in a fair amount of detail in the Django Built-in Signal Reference (djangoproject.com) so I won't go into too much depth on what they do, but they are as follows (skip to the first example):



pre_init
Occurs at the start of the process of initialising a model;

post_init
Occurs at the end of the process of initialising a model;

pre_save
Occurs at the start of the process of saving a model;

post_save
Occurs at the end of the process of saving a model;

pre_delete
Occurs at the start of the process of deleting a model;

post_delete
Occurs at the end of the process of deleting a model;

class_prepared
Internally used, occurring when a model class has been registered with Django;

post_syncdb
Occurs after syncdb installs an application;

request_started
Occurs just before Django starts to process a request;

request_finished
Occurs just after Django processes a request;

got_request_exception
Occurs when Django encounters an exception whilst processing a request;

template_rendered
Occurs only whilst running tests when Django renders a template;

comment_will_be_posted
Occurs if you have Django's comment app installed and a comment is just about to be saved;

comment_was_posted
Occurs if you have Django's comment app installed and a comment has just been saved;

comment_was_flagged
Occurs if you have Django's comment app installed and a comment has had a flag set (i.e. a moderator just approved it).


For my first example this week I'm going to be looking at the post_save signal and how it can be used to ensure that a User object always has a profile instance associated with it.


# File: test_signal/models.py
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save

class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
allow_our_emails = models.BooleanField(default=False)
allow_other_emails = models.BooleanField(default=False)

def ensure_profile_exists(sender, **kwargs):
if kwargs.get('created', False):
UserProfile.objects.create(user=kwargs.get('instance'))

post_save.connect(ensure_profile_exists, sender=User)

# File: (somewhere in) settings.py
AUTH_PROFILE_MODULE = "test_signal"

As you can see this defines a fairly simple profile model (supposedly for storing a user's e-mail preferences), the key part of this comes from the function ensure_profile_exists. With all signals it can only be guaranteed that the sender keyword argument will be present therefore we also need to catch everything else using **kwargs. Inside the function we use the fact that post_save is supposed to provide the arguments created and instance to check if the save was for a new object and if so to assign the created instance to the user property of a freshly created UserProfile object.


Finally we connect our function to the post_save signal with instructions to only receive signals when the sending class is User


If the test_signal app is now added to the list of installed apps in settings.py then all new users created will now have an associated profile created also.


The second example for this week demonstrates how you can create your very own signals for code to listen to.


# File: test_signal/signals.py
from django.dispatch import Signal

form_post_received = Signal(providing_args=["path","arguments",]

Here we tell Django that we want form_post_received to be a signal and that it should provide the arguments path and arguments. Now if we add the following:


# File: test_signal/views.py
from signals import form_post_received
from django.conf import settings

def view1(request):
if request.method == "POST":
form_post_received.send(sender=request, path=request.path, arguments=request.POST)
# Continue to do stuff with the form.

def view2(request):
if request.method == "POST":
form_post_received.send(sender=request, path=request.path, arguments=request.POST)
# Continue to do stuff with the form.

def log_form_received(sender, **kwargs):
print "Form Received at %(path)s with arguments %(arguments)s" % kwargs

if settings.DEBUG:
form_post_received.connect(log_form_received)

When pointing views at view1 and view2 whenever the server is in debug mode extra information should be output to the terminal informing us of whenever a form has been posted to either view.


This is done by calling send on our signal along with an argument for sender, in this case we use the request that triggered the view, along with values for each of the other arguments we specified. Django then passes them on to each function that has connected to our signal, which in this case is our logging function (but only when the server is in debug mode, otherwise connect will never be called).


That concludes this weeks tutorial on signals. Next week we shall be looking at the useful functions Django has built in for performing pagination.

UPDATE:There is now a second part to this tutorial talking about more advanced uses of signals. Check it out at Signals - Part 2: Return to sender