| 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 | #--------------------------------------------------------------------------- |