]>
Commit | Line | Data |
---|---|---|
d14a1e28 RD |
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. | |
1fded56b | 15 | |
d14a1e28 RD |
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. | |
1fded56b | 20 | |
d14a1e28 RD |
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 | #--------------------------------------------------------------------------- |