Ticket #4561: dispatch-smart-signals.patch
File dispatch-smart-signals.patch, 24.8 KB (added by , 17 years ago) |
---|
-
django/dispatch/weaksets.py
=== added file 'django/dispatch/weaksets.py'
1 from weakref import WeakKeyDictionary 2 3 class WeakSet(object): 4 def __init__(self, initial_vals=None): 5 self._map = WeakKeyDictionary() 6 if initial_vals is not None: 7 self.update(initial_vals) 8 self.remove = self._map.__delitem__ 9 self.add = self._map.setdefault 10 11 def update(self, iterable): 12 map(self._map.setdefault, iterable) 13 14 def __len__(self): 15 return len(self._map) 16 17 def __iter__(self): 18 return iter(self._map) 19 20 def discard(self, key): 21 self._map.pop(key, None) 22 23 def __nonzero__(self): 24 return bool(self._map) 25 26 def __repr__(self): 27 return "WeakSet(%r)" % self._map.keys() 28 29 def __str__(self): 30 return "WeakSet(%s)" % self._map.keys() 31 32 def __contains__(self, key): 33 return key in self._map -
tests/regressiontests/dispatch/tests/test_new_signals.py
=== added file 'tests/regressiontests/dispatch/tests/test_new_signals.py'
1 from django import dispatch 2 from django.dispatch import dispatcher 3 from unittest import TestCase 4 5 class base(TestCase): 6 7 def assertIn(self, needle, haystack, msg=None): 8 if msg is None: 9 msg = "%s wasn't in %s" % (needle, haystack) 10 self.assertTrue((needle in haystack), msg=msg) 11 12 class test_signal(base): 13 14 kls = staticmethod(dispatch.signal) 15 16 def test_str(self): 17 self.assertEqual(str(self.kls("asdf")), "asdf") 18 19 def test_repr(self): 20 o = self.kls("foon") 21 self.assertEqual(repr(o), "<signal signal='foon' @#%x>" % id(o)) 22 23 class test_mk_signal(base): 24 25 func = staticmethod(dispatch.mk_signal) 26 27 def test_it(self): 28 o = self.func("sig1") 29 self.assertEqual(o.__doc__, dispatch.signal.__doc__) 30 self.assertEqual(o.signal_name, "sig1") 31 docstring="mr. super-happy docstring says gimme candy" 32 o = self.func("sig2", docstring=docstring) 33 self.assertEqual(o.__doc__, docstring) 34 self.assertEqual(o.signal_name, 'sig2') 35 class dar(dispatch.signal):pass 36 self.assertEqual(self.func('sig3', signal_kls=dar).__class__, dar) 37 38 class protect_signals_state_mixin(object): 39 40 """mixin basically wiping the smart_signals state information clean after backing 41 up a copy; upon completion of a fixture, restores the state. 42 43 Note you cannot rely on existing connections/listeners in any tests using this mixin- 44 as said, the state is reset, then restored around the fixture 45 """ 46 47 def setUp(self): 48 self._known_sigs = dispatch.smart_signal.known_signals.items() 49 self._any_any = dispatch.smart_signal.any_any_count 50 self._all_known_senders = dispatch.smart_signal.all_known_senders.items() 51 dispatch.smart_signal.known_signals.clear() 52 dispatch.smart_signal.any_any_count = 0 53 dispatch.smart_signal.all_known_senders.clear() 54 55 def tearDown(self): 56 dispatch.smart_signal.known_signals.clear() 57 dispatch.smart_signal.known_signals.update(self._known_sigs) 58 dispatch.smart_signal.any_any_count = self._any_any 59 dispatch.smart_signal.all_known_senders.clear() 60 dispatch.smart_signal.all_known_senders.update(self._all_known_senders) 61 62 class smart_signals_mixin(protect_signals_state_mixin): 63 64 def mk_signal(self, name="test-signal"): 65 class tracking_sig(dispatch.smart_signal): 66 def __init__(self, *args): 67 dispatch.smart_signal.__init__(self, *args) 68 self.enables, self.disables = [], [] 69 70 def _enable_sender(self, sender, incr): 71 self.enables.append((sender, incr)) 72 73 def _disable_sender(self, sender, incr): 74 self.disables.append((sender, incr)) 75 76 return tracking_sig(name) 77 78 def mk_kls(self): 79 class foo(object): 80 def __init__(self, *args): 81 self.args = args 82 83 return foo 84 85 class test_smart_signal(smart_signals_mixin, base): 86 87 def test_behaviour(self): 88 kls, sig = self.mk_kls(), self.mk_signal() 89 self.assertEqual([], list(sig.known_senders)) 90 obj = kls() 91 sig.register_sender(obj) 92 self.assertEqual([obj], list(sig.known_senders)) 93 del obj 94 # assert weakref'd; jython probably won't like this, but oh well 95 self.assertEqual([], list(sig.known_senders)) 96 self.assertFalse(sig.enables) 97 self.assertFalse(sig.disables) 98 99 # force an Any in to force enables to track that it gets properly enabled, and that 100 # _enable_sender is invoked once with the higher incr count 101 obj = kls() 102 sig.enable_sender(dispatch.Any) 103 sig.register_sender(obj) 104 self.assertEqual([(obj, 1)], sig.enables) 105 obj2 = kls() 106 del obj 107 sig.enables = [] 108 sig.enable_sender(dispatch.Any) 109 110 # register a new instance; should be a single _enable_sender call, incr=2 due to 111 # 2 Any senders register. 112 113 sig.register_sender(obj2) 114 self.assertEqual([(obj2, 2)], sig.enables) 115 self.assertFalse(sig.disables) 116 117 class test_Any(smart_signals_mixin, base): 118 119 def test_register_sender(self): 120 self.assertRaises(TypeError, dispatch.Any.register_sender, self.mk_kls()()) 121 122 def test_enable_disable_sender(self): 123 kls = self.mk_kls() 124 obj1, obj2 = kls(), kls() 125 sig1, sig2 = self.mk_signal("sig1"), self.mk_signal("sig2") 126 sig1.register_sender(obj1) 127 sig2.register_sender(obj2) 128 129 self.assertFalse(sig1.enables) 130 self.assertFalse(sig2.enables) 131 132 # first test it's behaviour for a signal=Any, sender=specific 133 dispatch.Any.enable_sender(obj1) 134 self.assertEqual(sig1.enables, [(obj1, 1)]) 135 self.assertFalse(sig2.enables), self.assertFalse(sig2.disables) 136 dispatch.Any.disable_sender(obj1) 137 self.assertEqual(sig1.enables, [(obj1, 1)]) 138 self.assertEqual(sig1.disables, [(obj1, 1)]) 139 self.assertFalse(sig2.enables), self.assertFalse(sig2.disables) 140 141 dispatch.Any.enable_sender(obj2) 142 self.assertEqual(sig2.enables, [(obj2, 1)]) 143 dispatch.Any.disable_sender(obj2) 144 self.assertEqual(sig2.disables, [(obj2, 1)]) 145 sig1.enables, sig1.disables = [], [] 146 sig2.enables, sig2.disables = [], [] 147 148 # now test signal=Any, sender=Any behaviour. 149 dispatch.Any.enable_sender(dispatch.Any) 150 self.assertEqual(sig1.enables, [(obj1, 1)]) 151 self.assertEqual(sig2.enables, [(obj2, 1)]) 152 self.assertFalse(sig1.disables), self.assertFalse(sig2.disables) 153 154 dispatch.Any.enable_sender(dispatch.Any) 155 self.assertEqual(sig1.enables, [(obj1, 1)]*2) 156 self.assertEqual(sig2.enables, [(obj2, 1)]*2) 157 158 dispatch.Any.disable_sender(dispatch.Any, 2) 159 self.assertEqual(sig1.disables, [(obj1, 2)]) 160 self.assertEqual(sig2.disables, [(obj2, 2)]) -
tests/regressiontests/dispatch/tests/test_weakset.py
=== added file 'tests/regressiontests/dispatch/tests/test_weakset.py'
1 from django.dispatch.weaksets import WeakSet 2 from unittest import TestCase 3 4 if not hasattr(__builtins__, 'set'): 5 from sets import Set as set 6 7 class test_WeakSet(TestCase): 8 9 def mk_obj(self): 10 # can't weakref base object instances, hence subclassing 11 class foon(object): 12 pass 13 return foon() 14 15 def test_basics(self): 16 ws = WeakSet() 17 self.assertEqual(len(ws), 0) 18 ws.add(self.mk_obj()) 19 # should've gone out of scope immediately, since the only referant would be ws, 20 # which *better* be weakref'ing it. 21 self.assertEqual(len(ws), 0) 22 self.assertFalse(ws) 23 obj = self.mk_obj() 24 ws.add(obj) 25 self.assertEqual(len(ws), 1) 26 self.assertTrue(ws) 27 self.assertEqual(list(ws), [obj]) 28 obj2 = self.mk_obj() 29 ws.add(obj2) 30 self.assertEqual(set(ws), set([obj, obj2])) 31 32 del obj 33 self.assertEqual(list(ws), [obj2]) 34 del obj2 35 self.assertFalse(ws) 36 37 obj = self.mk_obj() 38 # check that it accepts an iterable 39 self.assertEqual(list(WeakSet([obj])), [obj]) 40 ws.discard(obj) 41 self.assertFalse(ws) 42 # no exception 43 ws.discard(obj) 44 ws.add(obj) 45 self.assertTrue(obj) 46 ws.remove(obj) 47 self.assertFalse(ws) 48 # keyerror 49 self.assertRaises(KeyError, ws.remove, obj) 50 51 # finally, update 52 objs = [self.mk_obj() for x in xrange(3)] 53 ws.update(objs) 54 self.assertEqual(set(ws), set(objs)) 55 56 self.assertTrue(objs[0] in ws) 57 self.assertFalse(self in ws) -
django/db/models/base.py
=== modified file 'django/db/models/base.py'
69 69 70 70 new_class._prepare() 71 71 72 # now we register the class with the signals it can have turned on; 73 signals.pre_init.register_sender(new_class) 74 signals.post_init.register_sender(new_class) 75 signals.pre_save.register_sender(new_class) 76 signals.post_save.register_sender(new_class) 77 72 78 register_models(new_class._meta.app_label, new_class) 73 79 # Because of the way imports happen (recursively), we may or may not be 74 80 # the first class for this model to register with the framework. There … … 95 101 return not self.__eq__(other) 96 102 97 103 def __init__(self, *args, **kwargs): 98 dispatcher.send(signal=signals.pre_init, sender=self.__class__, args=args, kwargs=kwargs)99 104 100 105 # There is a rather weird disparity here; if kwargs, it's set, then args 101 106 # overrides it. It should be one or the other; don't duplicate the work … … 164 169 pass 165 170 if kwargs: 166 171 raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0] 167 dispatcher.send(signal=signals.post_init, sender=self.__class__, instance=self)168 172 169 173 def add_to_class(cls, name, value): 170 174 if name == 'Admin': … … 199 203 _prepare = classmethod(_prepare) 200 204 201 205 def save(self): 202 dispatcher.send(signal=signals.pre_save, sender=self.__class__, instance=self)203 204 206 non_pks = [f for f in self._meta.fields if not f.primary_key] 205 207 cursor = connection.cursor() 206 208 … … 252 254 transaction.commit_unless_managed() 253 255 254 256 # Run any post-save hooks. 255 dispatcher.send(signal=signals.post_save, sender=self.__class__, instance=self)256 257 257 258 save.alters_data = True 258 259 -
django/db/models/signals.py
=== modified file 'django/db/models/signals.py'
1 class_prepared = object() 2 3 pre_init= object() 4 post_init = object() 5 6 pre_save = object() 7 post_save = object() 8 9 pre_delete = object() 10 post_delete = object() 11 12 post_syncdb = object() 1 from django.dispatch import mk_signal, mk_pre_post_signals 2 from django.dispatch.dispatcher import send 3 4 class_prepared = mk_signal('class_prepared') 5 6 class _init_mixin(object): 7 """one of two signals fired when a new model instance is created 8 9 pre_init is fired when the instance is being created, and is passed two keyword args: 10 @param args: positional args passed to __init__ 11 @param kwargs: dictionary of keyword args passed to __init__ 12 13 post_init is fired after the instance is fully created, and is passed a signal keyword arg: 14 @param instance: newly created instance 15 """ 16 17 def _pre_replacement(self, func, sender): 18 def wrapped(internal_self, *args, **kwargs): 19 send(signal=pre_init, sender=internal_self.__class__, args=args, kwargs=kwargs) 20 func(internal_self, *args, **kwargs) 21 return wrapped 22 23 def _post_replacement(self, func, sender): 24 def wrapped(internal_self, *args, **kwargs): 25 func(internal_self, *args, **kwargs) 26 send(signal=post_init, sender=internal_self.__class__, instance=internal_self) 27 return wrapped 28 29 def _both_replacement(self, func, sender): 30 def wrapped(internal_self, *args, **kwargs): 31 send(signal=pre_init, sender=internal_self.__class__, args=args, kwargs=kwargs) 32 func(internal_self, *args, **kwargs) 33 send(signal=post_init, sender=internal_self.__class__, instance=internal_self) 34 return wrapped 35 36 pre_init, post_init = mk_pre_post_signals('__init__', 'pre_init', 'post_init', _init_mixin) 37 38 class _save_mixin(object): 39 40 def _pre_replacement(self, func, sender): 41 def wrapped(internal_self, *args, **kwargs): 42 send(signal=pre_save, sender=internal_self.__class__, instance=internal_self) 43 func(internal_self, *args, **kwargs) 44 wrapped.alters_data = True 45 return wrapped 46 47 def _post_replacement(self, func, sender): 48 def wrapped(internal_self, *args, **kwargs): 49 func(internal_self, *args, **kwargs) 50 send(signal=post_save, sender=internal_self.__class__, instance=internal_self) 51 wrapped.alters_data = True 52 return wrapped 53 54 def _both_replacement(self, func, sender): 55 def wrapped(internal_self, *args, **kwargs): 56 send(signal=pre_save, sender=internal_self.__class__, instance=internal_self) 57 func(internal_self, *args, **kwargs) 58 send(signal=post_save, sender=internal_self.__class__, instance=internal_self) 59 wrapped.alters_data = True 60 return wrapped 61 62 pre_save, post_save = mk_pre_post_signals('save', 'pre_save', 'post_save', _save_mixin) 63 64 pre_delete = mk_signal('pre_delete') 65 post_delete = mk_signal('post_delete') 66 67 post_syncdb = mk_signal('post_syncdb') -
django/dispatch/__init__.py
=== modified file 'django/dispatch/__init__.py'
4 4 __author__ = "Patrick K. O'Brien" 5 5 __license__ = "BSD-style, see license.txt for details" 6 6 7 from weakref import WeakKeyDictionary 8 from weaksets import WeakSet 9 10 class signal(object): 11 12 def __init__(self, signal_name): 13 self.signal_name = signal_name 14 15 def __str__(self): 16 return self.signal_name 17 18 def __repr__(self): 19 return "<signal signal=%r @#%x>" % (self.signal_name, id(self)) 20 21 def mk_signal(signal_name, *args, **kwargs): 22 docstring = kwargs.pop("docstring", None) 23 signal_kls = kwargs.pop("signal_kls", signal) 24 if docstring is not None: 25 class signal_kls(signal_kls): 26 __doc__ = docstring 27 return signal_kls(signal_name, *args, **kwargs) 28 29 30 class smart_signal(signal): 31 32 # tracks known senders (keys), and the 'Any signal' count per sender 33 # signal=Any, sender=sender 34 all_known_senders = WeakKeyDictionary() 35 36 # 'Any signal, Any sender' count; tracked for new signal/sender initialization 37 any_any_count = 0 38 39 # tracks known signals, and the # of signal=signal, sender=Any 40 # one exemption to this; Any is left out of known_signals. 41 known_signals = WeakKeyDictionary() 42 43 # note per instance, there is a 'known_senders'; tracks the known senders 44 # of a specific signal (just that). This is required to be tracked so 45 # that a sender can't be registered twice (will trash the ref cnts) 46 47 def __init__(self, signal_name): 48 signal.__init__(self, signal_name) 49 self.known_senders = WeakSet() 50 # set the # of sender=any, signal=self count. 51 self.known_signals[self] = self.any_any_count 52 53 def register_sender(self, sender): 54 if sender in self.known_senders: 55 # already known; can occur for instances that have a shared known_senders. 56 return 57 self.known_senders.add(sender) 58 59 # find the incref count from the misc Any currently listening 60 count = self.all_known_senders.setdefault(sender, 0) + self.known_signals[self] 61 if count: 62 self.enable_sender(sender, incr=count) 63 64 def enable_sender(self, sender, incr=1): 65 if sender is Any: 66 self.known_signals[self] += 1 67 for sender in self.known_senders: 68 self._enable_sender(sender, incr) 69 return 70 self._enable_sender(sender, incr) 71 72 def disable_sender(self, sender, decr=1): 73 if sender is Any: 74 self.known_signals[self] -= 1 75 # on the offchance a sender goes away when nothing is listening. 76 # *probably* not sanely possible 77 for sender in tuple(self.known_senders): 78 self._disable_sender(sender, decr) 79 return 80 81 self._disable_sender(sender, decr) 82 83 def _enable_sender(self, sender, incr): 84 raise NotImplementedError(self, '_enable_sender') 85 86 def _disable_sender(self, sender, decr): 87 raise NotImplementedError(self, '_disable_sender') 88 89 class Any(smart_signal): 90 91 """Singleton used to signal either "Any Sender" or "Any Signal" 92 93 The Any object can be used with connect, disconnect, 94 send, or sendExact to signal that the parameter given 95 Any should react to all senders/signals, not just 96 a particular sender/signal. 97 """ 98 99 def __init__(self): 100 signal.__init__(self, "Any") 101 self.known_senders = smart_signal.all_known_senders 102 103 def register_sender(self, sender): 104 raise TypeError("you cannot register a sender for Any") 105 106 def enable_sender(self, sender, incr=1): 107 if sender is self: 108 for signal in self.known_signals.iterkeys(): 109 signal.enable_sender(self, incr=incr) 110 smart_signal.any_any_count += incr 111 else: 112 for signal in self.known_signals.iterkeys(): 113 if sender in signal.known_senders: 114 signal.enable_sender(sender, incr=incr) 115 116 def disable_sender(self, sender, decr=1): 117 if sender is self: 118 for signal in self.known_signals.iterkeys(): 119 signal.disable_sender(self, decr=decr) 120 smart_signal.any_any_count -= decr 121 assert smart_signal.any_any_count >= 0 122 else: 123 for signal in self.known_signals.iterkeys(): 124 if sender in signal.known_senders: 125 signal.disable_sender(sender, decr=decr) 126 127 Any = Any() 128 129 class _pre_post_signal(smart_signal): 130 131 def __init__(self, signal_name, method_name, is_pre, known_senders=None): 132 smart_signal.__init__(self, signal_name) 133 self._method_name = method_name 134 if known_senders is not None: 135 self.known_senders = known_senders 136 self.is_pre = is_pre 137 138 def _enable_sender(self, sender, incr): 139 original_func = func = getattr(sender, self._method_name) 140 precount = getattr(original_func, "_listening_pre_refcnt", 0) 141 postcount = getattr(original_func, "_listening_post_refcnt", 0) 142 143 if self.is_pre: 144 if postcount: 145 if not precount: 146 func = self._both_replacement(func._listening_wrapped, sender) 147 original_func = original_func._listening_wrapped 148 elif not precount: 149 func = self._pre_replacement(func, sender) 150 precount += incr 151 else: 152 if precount: 153 if not postcount: 154 func = self._both_replacement(func._listening_wrapped, sender) 155 original_func = original_func._listening_wrapped 156 elif not postcount: 157 func = self._post_replacement(func, sender) 158 postcount += incr 159 160 func._listening_pre_refcnt = precount 161 func._listening_post_refcnt = postcount 162 func._listening_refcnt = precount + postcount 163 164 if func is not original_func: 165 setattr(sender, self._method_name, func) 166 func._listening_wrapped = original_func 167 168 def _disable_sender(self, sender, decr): 169 func = original_func = getattr(sender, self._method_name) 170 171 # shortcut to simplify logic. 172 if func._listening_refcnt <= decr: 173 setattr(sender, self._method_name, func._listening_wrapped) 174 return 175 176 precount = func._listening_pre_refcnt 177 postcount = func._listening_post_refcnt 178 179 if self.is_pre: 180 precount -= decr 181 if not precount: 182 func = self._post_replacement(func, sender) 183 func._listening_refcnt = func._listening_post_refcnt = postcount 184 func._listening_pre_refcnt = precount 185 else: 186 postcount -= decr 187 if not postcount: 188 func = self._pre_replacement(func, sender) 189 func._listening_refcnt = func._listening_pre_refcnt = precount 190 func._listening_post_refcnt = postcount 191 192 if func is not original_func: 193 seattr(sender, self._method_name, func) 194 func._listening_func = original_func._listening_wrapped 195 196 def _pre_replacement(self, original_func, sender): 197 raise NotImplementedError(self, "_pre_replacement") 198 199 def _post_replacement(self, original_func, sender): 200 raise NotImplementedError(self, "_post_replacement") 201 202 def _both_replacement(self, original_func, sender): 203 raise NotImplementedError(self, "_both_replacement") 204 205 def mk_pre_post_signals(method_name, pre_signal_name, post_signal_name, mixin_kls): 206 class mixin(mixin_kls, _pre_post_signal): 207 __doc__ = mixin_kls.__doc__ 208 pass 209 pre = mixin(pre_signal_name, method_name, True) 210 pre.__name__ = pre_signal_name 211 post = mixin(post_signal_name, method_name, False)# 212 post.__name__ = post_signal_name 213 return pre, post -
django/dispatch/dispatcher.py
=== modified file 'django/dispatch/dispatcher.py'
26 26 vs. the original code.) 27 27 """ 28 28 import types, weakref 29 from django.dispatch import saferef, robustapply, errors 29 # Any is imported for backwards compatibility API wise 30 from django.dispatch import saferef, robustapply, errors, mk_signal, Any 30 31 31 32 __author__ = "Patrick K. O'Brien <pobrien@orbtech.com>" 32 33 __cvsid__ = "$Id: dispatcher.py,v 1.9 2005/09/17 04:55:57 mcfletch Exp $" 33 34 __version__ = "$Revision: 1.9 $"[11:-2] 34 35 35 36 class _Parameter: 37 """Used to represent default parameter values.""" 38 def __repr__(self): 39 return self.__class__.__name__ 40 41 class _Any(_Parameter): 42 """Singleton used to signal either "Any Sender" or "Any Signal" 43 44 The Any object can be used with connect, disconnect, 45 send, or sendExact to signal that the parameter given 46 Any should react to all senders/signals, not just 47 a particular sender/signal. 48 """ 49 Any = _Any() 50 51 class _Anonymous(_Parameter): 36 Anonymous = mk_signal('Anonymous', docstring= 52 37 """Singleton used to signal "Anonymous Sender" 53 38 54 39 The Anonymous object is used to signal that the sender … … 65 50 in either function then all messages are routed 66 51 as though there was a single sender (Anonymous) 67 52 being used everywhere. 68 """ 69 Anonymous = _Anonymous() 53 """) 70 54 71 55 WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref) 72 56 … … 137 121 138 122 signals = connections.setdefault(senderkey, {}) 139 123 124 enabler = getattr(signal, 'enable_sender', None) 125 if enabler is not None: 126 enabler(sender) 140 127 # Keep track of senders for cleanup. 141 128 # Is Anonymous something we want to clean up? 142 129 if sender not in (None, Anonymous, Any): … … 203 190 ) 204 191 if weak: receiver = saferef.safeRef(receiver) 205 192 senderkey = id(sender) 193 disabler = getattr(signal, 'disable_sender', None) 194 if disabler is not None: 195 disabler(sender) 206 196 try: 207 197 signals = connections[senderkey] 208 198 receivers = signals[signal] -
tests/regressiontests/dispatch/tests/__init__.py
=== modified file 'tests/regressiontests/dispatch/tests/__init__.py'
5 5 from test_dispatcher import * 6 6 from test_robustapply import * 7 7 from test_saferef import * 8 from test_new_signals import * 9 from test_weakset import *