Skip to content

Instantly share code, notes, and snippets.

@zacharyvoase
Created March 19, 2009 18:27
Show Gist options
  • Save zacharyvoase/81989 to your computer and use it in GitHub Desktop.
Save zacharyvoase/81989 to your computer and use it in GitHub Desktop.
Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py (revision 10088)
+++ django/db/models/base.py (working copy)
@@ -188,7 +188,11 @@
new_class._prepare()
register_models(new_class._meta.app_label, new_class)
-
+
+ # Add the default signals to the new model. A model's signals will be
+ # available as the 'signals' attribute on the model class.
+ new_class.add_to_class('signals', signals.ModelSignals)
+
# Because of the way imports happen (recursively), we may or may not be
# the first time this model tries to register with the framework. There
# should only be one class for each model, so we always return the
Index: django/db/models/signals.py
===================================================================
--- django/db/models/signals.py (revision 10088)
+++ django/db/models/signals.py (working copy)
@@ -1,5 +1,9 @@
+from functools import partial, wraps
+import re
+
from django.dispatch import Signal
+
class_prepared = Signal(providing_args=["class"])
pre_init = Signal(providing_args=["instance", "args", "kwargs"])
@@ -12,3 +16,46 @@
post_delete = Signal(providing_args=["instance"])
post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
+
+
+class ModelSignals(object):
+
+ """
+ Provides a set of decorators for registering model-specific signal handlers.
+
+ A model class's individual ``ModelSignals`` instance is available as its
+ `signals` attribute. This has a set of instance methods which may be used as
+ decorators for connecting signal handlers to the model's
+ """
+
+ def __init__(self, model):
+ self.model = model
+
+ def deco_connect(self, signal_name, function):
+
+ @wraps(function)
+ def handler(*args, **kwargs):
+ # This ModelSignal instance is attached to a model, so we already
+ # know what the sender will be, and can safely discard it.
+ kwargs.pop('sender', None)
+ # This is the instance which is being initialized, saved or deleted.
+ instance = kwargs.pop('instance', None)
+
+ if instance:
+ return function(instance, *args, **kwargs)
+ return function(*args, **kwargs)
+
+ # Using weak=False is necessary to prevent the temporarily-created
+ # handler function from being garbage collected.
+ globals()[signal_name].connect(handler, sender=self.model, weak=False)
+
+ return function
+
+ def __getattribute__(self, attribute):
+ if not re.match(r'^(pre|post)_(init|save|delete)', attribute):
+ return object.__getattribute__(self, attribute)
+ return partial(self.deco_connect, attribute)
+
+ @classmethod
+ def contribute_to_class(cls, class_, name):
+ setattr(class_, name, cls(class_))
\ No newline at end of file
Index: django/dispatch/dispatcher.py
===================================================================
--- django/dispatch/dispatcher.py (revision 10088)
+++ django/dispatch/dispatcher.py (working copy)
@@ -123,7 +123,47 @@
for idx, (r_key, _) in enumerate(self.receivers):
if r_key == lookup_key:
del self.receivers[idx]
-
+
+ def receive(self, *args, **kwargs):
+ """Connect receiver to sender for signal (decorator)
+
+ Can be used in two different ways:
+
+ @signal.receive
+ def receiver(*args, **kwargs):
+ pass
+
+ @signal.receive(sender=SenderClass)
+ def receiver(*args, **kwargs):
+ pass
+
+ In the first form, this method simply connects the signal and returns
+ the original receiver. In the second form, this method returns a
+ connector function which acts as a closure, capturing the passed keyword
+ arguments; this will then connect and return the receiver.
+ """
+
+ # Requires either only one argument (i.e. the first form in the above
+ # docstring), which will be the receiver, or no arguments.
+ if len(args) > 1:
+ raise TypeError('Illegal number of arguments for decorator')
+
+ # This means that the first form of the above docstring was used, so the
+ # handler should be connected as-is. Because this is a decorator, it is
+ # necessary to return the function afterwards.
+ if args:
+ self.connect(*args, **kwargs)
+ return args[0]
+
+ # This means the second form was used, in which case a further decorator
+ # must be made and returned which will simply close over the passed
+ # kwargs and connect the function it receives.
+ else:
+ def connector(function):
+ self.connect(function, **kwargs)
+ return function
+ return connector
+
def send(self, sender, **named):
"""Send signal from sender to all connected receivers.
# -*- coding: utf-8 -*-
# The standard imports, along with the signals module. This is used in the very
# last signal handler example, where a raw Signal object is used to handle
from django.db import models
from django.db.models import signals
# A pretty standard model definition.
class MyModel(models.Model):
field1 = models.IntegerField(default=0)
field2 = models.CharField(max_length=128)
# The following two examples show how the model-specific signals might work;
# note the 'signals' class attribute on the MyModel class which provides these
# signals and also the fact that the 'instance' keyword argument has
# automatically been turned into a positional argument (in the interest of both
# brevity and DRought).
@MyModel.signals.pre_save
def signal_handler1(mymodel, *args, **kwargs):
"""Connected to MyModel's pre_save event."""
do_something(mymodel)
@MyModel.signals.post_save
def signal_handler2(mymodel, *args, **kwargs):
"""Connected to MyModel's post_save event."""
do_something_else(mymodel)
# Note that the following connects the signal_handler3 function to the global
# pre_save signal, although it does so in a more verbose way. This is a more
# general pattern of usage which will make it possible to use the same decorator
# syntax but with virtually any signal (including the class_prepared and the
# post_syncdb signals).
@signals.pre_save.receive
def signal_handler3(instance=None, sender=None, *args, **kwargs):
"""Connected to every pre_save event."""
do_something_again(model_instance)
# This is similar to the example above, except some keyword arguments are
# specified in the decorator statement. Any additional kwargs will be passed to
# the signal's connect() method; in this case the signal_handler4 function will
# only receive signals sent by MyModel. This is functionally equivalent to
# decorating using ``MyModel.signals.pre_save()``.
@signals.pre_save.receive(sender=MyModel)
def signal_handler4(instance=None, sender=None, *args, **kwargs):
"""Connected to every pre_save event."""
do_something_again(model_instance)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment