]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/pubsub.py
It *does* work now
[wxWidgets.git] / wxPython / wx / lib / pubsub.py
1 #---------------------------------------------------------------------------
2 # Name: wxPython.lib.pubsub
3 # Purpose: The Publish/Subscribe framework used by evtmgr.EventManager
4 #
5 # Author: Robb Shecter and Robin Dunn
6 #
7 # Created: 12-December-2002
8 # RCS-ID: $Id$
9 # Copyright: (c) 2002 by db-X Corporation
10 # Licence: wxWindows license
11 #---------------------------------------------------------------------------
12 """
13 This module has classes for implementing the Publish/Subscribe design
14 pattern.
15
16 It's a very flexible PS implementation: The message topics are tuples
17 of any length, containing any objects (that can be used as hash keys).
18 A subscriber's topic matches any message topic for which it's a
19 sublist.
20
21 It also has many optimizations to favor time efficiency (ie., run-time
22 speed). I did this because I use it to support extreme uses. For
23 example, piping every wxWindows mouse event through to multiple
24 listeners, and expecting the app to have no noticeable slowdown. This
25 has made the code somewhat obfuscated, but I've done my best to
26 document it.
27
28 The Server and Message classes are the two that clients interact
29 with..
30
31 This module is compatible with Python 2.1.
32
33 Author: Robb Shecter
34 """
35
36 #---------------------------------------------------------------------------
37
38 class Publisher:
39 """
40 The publish/subscribe server. This class is a Singleton.
41 """
42 def __init__(self):
43 self.topicDict = {}
44 self.functionDict = {}
45 self.subscribeAllList = []
46 self.messageCount = 0
47 self.deliveryCount = 0
48
49
50 #
51 # Public API
52 #
53
54 def subscribe(self, topic, listener):
55 """
56 Add the given subscription to the list. This will
57 add an entry recording the fact that the listener wants
58 to get messages for (at least) the given topic. This
59 method may be called multiple times for one listener,
60 registering it with many topics. It can also be invoked
61 many times for a particular topic, each time with a
62 different listener.
63
64 listener: expected to be either a method or function that
65 takes zero or one parameters. (Not counting 'self' in the
66 case of methods. If it accepts a parameter, it will be given
67 a reference to a Message object.
68
69 topic: will be converted to a tuple if it isn't one.
70 It's a pattern matches any topic that it's a sublist
71 of. For example, this pattern:
72
73 ('sports',)
74
75 would match these:
76
77 ('sports',)
78 ('sports', 'baseball')
79 ('sports', 'baseball', 'highscores')
80
81 but not these:
82
83 ()
84 ('news')
85 (12345)
86 """
87 if not callable(listener):
88 raise TypeError('The P/S listener, '+`listener`+', is not callable.')
89 aTopic = Topic(topic)
90
91 # Determine now (at registration time) how many parameters
92 # the listener expects, and get a reference to a function which
93 # calls it correctly at message-send time.
94 callableVersion = self.__makeCallable(listener)
95
96 # Add this tuple to a list which is in a dict keyed by
97 # the topic's first element.
98 self.__addTopicToCorrectList(aTopic, listener, callableVersion)
99
100 # Add to a dict in order to speed-up unsubscribing.
101 self.__addFunctionLookup(listener, aTopic)
102
103
104 def unsubscribe(self, listener):
105 """
106 Remove the given listener from the registry,
107 for all topics that it's associated with.
108 """
109 if not callable(listener):
110 raise TypeError('The P/S listener, '+`listener`+', is not callable.')
111 topicList = self.getAssociatedTopics(listener)
112 for aTopic in topicList:
113 subscriberList = self.__getTopicList(aTopic)
114 listToKeep = []
115 for subscriber in subscriberList:
116 if subscriber[0] != listener:
117 listToKeep.append(subscriber)
118 self.__setTopicList(aTopic, listToKeep)
119 self.__delFunctionLookup(listener)
120
121
122 def getAssociatedTopics(self, listener):
123 """
124 Return a list of topics the given listener is
125 registered with.
126 """
127 return self.functionDict.get(listener, [])
128
129
130 def sendMessage(self, topic, data=None):
131 """
132 Relay a message to registered listeners.
133 """
134 aTopic = Topic(topic)
135 message = Message(aTopic.items, data)
136 topicList = self.__getTopicList(aTopic)
137
138 # Send to the matching topics
139 for subscriber in topicList:
140 if subscriber[1].matches(aTopic):
141 subscriber[2](message)
142
143 # Send to any listeners registered for ALL
144 for subscriber in self.subscribeAllList:
145 subscriber[2](message)
146
147
148 #
149 # Private methods
150 #
151
152 def __makeCallable(self, function):
153 """
154 Return a function that is what the server
155 will actually call.
156
157 This is a time optimization: this removes a test
158 for the number of parameters from the inner loop
159 of sendMessage().
160 """
161 parameters = self.__parameterCount(function)
162 if parameters == 0:
163 # Return a function that calls the listener
164 # with no arguments.
165 return lambda m, f=function: f()
166 elif parameters == 1:
167 # Return a function that calls the listener
168 # with one argument (which will be the message).
169 return lambda m, f=function: f(m)
170 else:
171 raise TypeError('The publish/subscribe listener, '+`function`+', has wrong parameter count')
172
173
174 def __parameterCount(self, callableObject):
175 """
176 Return the effective number of parameters required
177 by the callable object. In other words, the 'self'
178 parameter of methods is not counted.
179 """
180 try:
181 # Try to handle this like a method
182 return callableObject.im_func.func_code.co_argcount - 1
183 except AttributeError:
184 pass
185
186 try:
187 # Try to handle this like a function
188 return callableObject.func_code.co_argcount
189 except AttributeError:
190 raise 'Cannot determine if this is a method or function: '+str(callableObject)
191
192 def __addFunctionLookup(self, aFunction, aTopic):
193 try:
194 aList = self.functionDict[aFunction]
195 except KeyError:
196 aList = []
197 self.functionDict[aFunction] = aList
198 aList.append(aTopic)
199
200
201 def __delFunctionLookup(self, aFunction):
202 try:
203 del self.functionDict[aFunction]
204 except KeyError:
205 print 'Warning: listener not found. Logic error in PublishSubscribe?', aFunction
206
207
208 def __addTopicToCorrectList(self, topic, listener, callableVersion):
209 if len(topic.items) == 0:
210 self.subscribeAllList.append((listener, topic, callableVersion))
211 else:
212 self.__getTopicList(topic).append((listener, topic, callableVersion))
213
214
215 def __getTopicList(self, aTopic):
216 """
217 Return the correct sublist of subscribers based on the
218 given topic.
219 """
220 try:
221 elementZero = aTopic.items[0]
222 except IndexError:
223 return self.subscribeAllList
224
225 try:
226 subList = self.topicDict[elementZero]
227 except KeyError:
228 subList = []
229 self.topicDict[elementZero] = subList
230 return subList
231
232
233 def __setTopicList(self, aTopic, aSubscriberList):
234 try:
235 self.topicDict[aTopic.items[0]] = aSubscriberList
236 except IndexError:
237 self.subscribeAllList = aSubscriberList
238
239
240 def __call__(self):
241 return self
242
243
244 # Create an instance with the same name as the class, effectivly
245 # hiding the class object so it can't be instantiated any more. From
246 # this point forward any calls to Publisher() will invoke the __call__
247 # of this instance which just returns itself.
248 #
249 # The only flaw with this approach is that you can't derive a new
250 # class from Publisher without jumping through hoops. If this ever
251 # becomes an issue then a new Singleton implementaion will need to be
252 # employed.
253 Publisher = Publisher()
254
255
256 #---------------------------------------------------------------------------
257
258 class Message:
259 """
260 A simple container object for the two components of
261 a message; the topic and the data.
262 """
263 def __init__(self, topic, data):
264 self.topic = topic
265 self.data = data
266
267 def __str__(self):
268 return '[Topic: '+`self.topic`+', Data: '+`self.data`+']'
269
270
271 #---------------------------------------------------------------------------
272
273 class Topic:
274 """
275 A class that represents a publish/subscribe topic.
276 Currently, it's only used internally in the framework; the
277 API expects and returns plain old tuples.
278
279 It currently exists mostly as a place to keep the matches()
280 function. This function, though, could also correctly be
281 seen as an attribute of the P/S server. Getting rid of this
282 class would also mean one fewer object instantiation per
283 message send.
284 """
285
286 listType = type([])
287 tupleType = type(())
288
289 def __init__(self, items):
290 # Make sure we have a tuple.
291 if type(items) == self.__class__.listType:
292 items = tuple(items)
293 elif type(items) != self.__class__.tupleType:
294 items = (items,)
295 self.items = items
296 self.length = len(items)
297
298
299 def matches(self, aTopic):
300 """
301 Consider myself to be a topic pattern,
302 and return True if I match the given specific
303 topic. For example,
304 a = ('sports')
305 b = ('sports','baseball')
306 a.matches(b) --> 1
307 b.matches(a) --> 0
308 """
309 # The question this method answers is equivalent to;
310 # is my list a sublist of aTopic's? So, my algorithm
311 # is: 1) make a copy of the aTopic list which is
312 # truncated to the pattern's length. 2) Test for
313 # equality.
314 #
315 # This algorithm may be somewhat memory-intensive,
316 # because it creates a temporary list on each
317 # call to match. A possible to-do would be to
318 # re-write this with a hand-coded loop.
319 return (self.items == aTopic.items[:self.length])
320
321
322 def __repr__(self):
323 import string
324 return '<Topic>' + string.join(map(repr, self.items), ', ') + '</Topic>'
325
326
327 def __eq__(self, aTopic):
328 """
329 Return True if I equal the given topic. We're considered
330 equal if our tuples are equal.
331 """
332 if type(self) != type(aTopic):
333 return 0
334 else:
335 return self.items == aTopic.items
336
337
338 def __ne__(self, aTopic):
339 """
340 Return False if I equal the given topic.
341 """
342 return not self == aTopic
343
344
345 #---------------------------------------------------------------------------
346
347
348 #
349 # Code for a simple command-line test
350 #
351 if __name__ == '__main__':
352
353 class SimpleListener:
354 def __init__(self, number):
355 self.number = number
356 def notify(self, message):
357 print '#'+str(self.number)+' got the message:', message
358
359 # Build a list of ten listeners.
360 lList = []
361 for x in range(10):
362 lList.append(SimpleListener(x))
363
364 server = Publisher()
365
366 # Everyone's interested in politics...
367 for x in lList:
368 Publisher().subscribe(topic='politics', listener=x.notify) # also tests singleton
369
370 # But only the first four are interested in trivia.
371 for x in lList[:4]:
372 server.subscribe(topic='trivia', listener=x.notify)
373
374 # This one subscribes to everything.
375 everythingListener = SimpleListener(999)
376 server.subscribe(topic=(), listener=everythingListener.notify)
377
378 # Now send out two messages, testing topic matching.
379 server.sendMessage(topic='trivia', data='What is the capitol of Oregon?')
380 server.sendMessage(topic=('politics','germany'), data='The Greens have picked up another seat in the Bundestag.')
381
382 #---------------------------------------------------------------------------