/
event.py
228 lines (203 loc) · 8.43 KB
/
event.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# Copyright (C) 2007-2010 Samuel Abels.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
A simple signal/event mechanism.
"""
from threading import Lock
from Exscript.util import weakmethod
class Event(object):
"""
A simple signal/event mechanism, to be used like this::
def mycallback(arg, **kwargs):
print arg, kwargs['foo']
myevent = Event()
myevent.connect(mycallback)
myevent.emit('test', foo = 'bar')
# Or just: myevent('test', foo = 'bar')
"""
def __init__(self):
"""
Constructor.
"""
# To save memory, we do NOT init the subscriber attributes with
# lists. Unfortunately this makes a lot of the code in this class
# more messy than it should be, but events are used so widely in
# Exscript that this change makes a huge difference to the memory
# footprint.
self.lock = None
self.weak_subscribers = None
self.hard_subscribers = None
def __call__(self, *args, **kwargs):
"""
Like emit().
"""
return self.emit(*args, **kwargs)
def connect(self, callback, *args, **kwargs):
"""
Connects the event with the given callback.
When the signal is emitted, the callback is invoked.
@note: The signal handler is stored with a hard reference, so you
need to make sure to call L{disconnect()} if you want the handler
to be garbage collected.
@type callback: object
@param callback: The callback function.
@type args: tuple
@param args: Optional arguments passed to the callback.
@type kwargs: dict
@param kwargs: Optional keyword arguments passed to the callback.
"""
if self.is_connected(callback):
raise AttributeError('callback is already connected')
if self.hard_subscribers is None:
self.hard_subscribers = []
self.hard_subscribers.append((callback, args, kwargs))
def listen(self, callback, *args, **kwargs):
"""
Like L{connect()}, but uses a weak reference instead of a
normal reference.
The signal is automatically disconnected as soon as the handler
is garbage collected.
@note: Storing signal handlers as weak references means that if
your handler is a local function, it may be garbage collected. To
prevent this, use L{connect()} instead.
@type callback: object
@param callback: The callback function.
@type args: tuple
@param args: Optional arguments passed to the callback.
@type kwargs: dict
@param kwargs: Optional keyword arguments passed to the callback.
@rtype: L{Exscript.util.weakmethod.WeakMethod}
@return: The newly created weak reference to the callback.
"""
if self.lock is None:
self.lock = Lock()
with self.lock:
if self.is_connected(callback):
raise AttributeError('callback is already connected')
if self.weak_subscribers is None:
self.weak_subscribers = []
ref = weakmethod.ref(callback, self._try_disconnect)
self.weak_subscribers.append((ref, args, kwargs))
return ref
def n_subscribers(self):
"""
Returns the number of connected subscribers.
@rtype: int
@return: The number of subscribers.
"""
hard = self.hard_subscribers and len(self.hard_subscribers) or 0
weak = self.weak_subscribers and len(self.weak_subscribers) or 0
return hard + weak
def _hard_callbacks(self):
return [s[0] for s in self.hard_subscribers]
def _weakly_connected_index(self, callback):
if self.weak_subscribers is None:
return None
weak = [s[0].get_function() for s in self.weak_subscribers]
try:
return weak.index(callback)
except ValueError:
return None
def is_connected(self, callback):
"""
Returns True if the event is connected to the given function.
@type callback: object
@param callback: The callback function.
@rtype: bool
@return: Whether the signal is connected to the given function.
"""
index = self._weakly_connected_index(callback)
if index is not None:
return True
if self.hard_subscribers is None:
return False
return callback in self._hard_callbacks()
def emit(self, *args, **kwargs):
"""
Emits the signal, passing the given arguments to the callbacks.
If one of the callbacks returns a value other than None, no further
callbacks are invoked and the return value of the callback is
returned to the caller of emit().
@type args: tuple
@param args: Optional arguments passed to the callbacks.
@type kwargs: dict
@param kwargs: Optional keyword arguments passed to the callbacks.
@rtype: object
@return: Returns None if all callbacks returned None. Returns
the return value of the last invoked callback otherwise.
"""
if self.hard_subscribers is not None:
for callback, user_args, user_kwargs in self.hard_subscribers:
kwargs.update(user_kwargs)
result = callback(*args + user_args, **kwargs)
if result is not None:
return result
if self.weak_subscribers is not None:
for callback, user_args, user_kwargs in self.weak_subscribers:
kwargs.update(user_kwargs)
# Even though WeakMethod notifies us when the underlying
# function is destroyed, and we remove the item from the
# the list of subscribers, there is no guarantee that
# this notification has already happened because the garbage
# collector may run while this loop is executed.
# Disabling the garbage collector temporarily also does
# not work, because other threads may be trying to do
# the same, causing yet another race condition.
# So the only solution is to skip such functions.
function = callback.get_function()
if function is None:
continue
result = function(*args + user_args, **kwargs)
if result is not None:
return result
def _try_disconnect(self, ref):
"""
Called by the weak reference when its target dies.
In other words, we can assert that self.weak_subscribers is not
None at this time.
"""
with self.lock:
weak = [s[0] for s in self.weak_subscribers]
try:
index = weak.index(ref)
except ValueError:
# subscriber was already removed by a call to disconnect()
pass
else:
self.weak_subscribers.pop(index)
def disconnect(self, callback):
"""
Disconnects the signal from the given function.
@type callback: object
@param callback: The callback function.
"""
if self.weak_subscribers is not None:
with self.lock:
index = self._weakly_connected_index(callback)
if index is not None:
self.weak_subscribers.pop(index)[0]
if self.hard_subscribers is not None:
try:
index = self._hard_callbacks().index(callback)
except ValueError:
pass
else:
self.hard_subscribers.pop(index)
def disconnect_all(self):
"""
Disconnects all connected functions from all signals.
"""
self.hard_subscribers = None
self.weak_subscribers = None