Quick summary
In the current Django setup, it is possible for a signal listener to register itself with the dispatcher (PyDispatcher) more than once.
The result is that the same function can end-up being called twice when a single signal is sent.
Quick example
testproj/testapp/models.py
class A(models.Model):
a = models.IntegerField(default=1)
def test_function_A(): print "Called listener"
dispatcher.connect(test_function_A, signal=signals.pre_init, sender=A)
Interactive session (./manage.py shell)
>>> from django.dispatch import dispatcher
>>> from django.db.models import signals
>>> from testproj.testapp.models import A
>>> dispatcher.getReceivers(sender=A, signal=signals.pre_init)
[<function test_function_A at 0x1203fb0>]
>>> from testapp.models import A
>>> dispatcher.getReceivers(sender=A, signal=signals.pre_init)
[<function test_function_A at 0x1203fb0>, <function test_function_A at 0x1206a30>]
>>> A()
Called listener
Called listener
<A: A object>
Deeper discussion
The problem seems to stem from the fact that Django puts itself in Python's path more than once. For example, for the project 'testproj', Python will look in /path/to/project/ and /path/to/project/testproj/. This, both testapp and testproj.testapp are valid.
Though these locations are both valid and reference the same content, they are actually treated as being different. Models seem to be treated as singletons, but other objects do not. I assume that this is because of the way in which models are loaded when Django fires-up.
>>> from testproj.testapp.models import A as full_path_A_class
>>> from testapp.models import A as short_path_A_class
>>> id(full_path_A_class), id(short_path_A_class)
(6797536, 6797536)
>>> from testproj.testapp.models import test_function_A as full_path_A_function
>>> from testapp.models import test_function_A as short_path_A_function
>>> id(full_path_A_function), id(short_path_A_function)
(18898800, 18960624)
Note in the last line of output that two different values are returned (18898800 != 18960624). Thus, the dispatcher.connect function has no way to determine that the listeners are the same.
The code responsible for the registration of listeners in PyDispatch can be found at django/dispatch/dispatcher.py:153-170; however, I'm not sure that we should go hacking on the PyDispatcher code. I'm honestly not sure what the solution to this one should be.
"Disclaimers"
The following things should be noted:
- I am using trunk for testing; however, I've also tried manually updating to PyDispatcher 2.0.0 and the problem still exists.
- All of my testing has been on Python 2.5. I assume that older versions will behave similarly.
- All of my testing has been done in the shell and with the built-in server.
- It is possible that this issue does not exist in a FCGI/mod_python deployment... I haven't tested those environments.