Mercurial > traipse_dev
comparison orpg/tools/pubsub.py @ 0:4385a7d0efd1 grumpy-goblin
Deleted and repushed it with the 'grumpy-goblin' branch. I forgot a y
author | sirebral |
---|---|
date | Tue, 14 Jul 2009 16:41:58 -0500 |
parents | |
children | dcae32e219f1 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4385a7d0efd1 |
---|---|
1 | |
2 #--------------------------------------------------------------------------- | |
3 """ | |
4 This module provides a publish-subscribe component that allows | |
5 listeners to subcribe to messages of a given topic. Contrary to the | |
6 original wxPython.lib.pubsub module (which it is based on), it uses | |
7 weak referencing to the subscribers so the lifetime of subscribers | |
8 is not affected by Publisher. Also, callable objects can be used in | |
9 addition to functions and bound methods. See Publisher class docs for | |
10 more details. | |
11 | |
12 Thanks to Robb Shecter and Robin Dunn for having provided | |
13 the basis for this module (which now shares most of the concepts but | |
14 very little design or implementation with the original | |
15 wxPython.lib.pubsub). | |
16 | |
17 The publisher is a singleton instance of the PublisherClass class. You | |
18 access the instance via the Publisher object available from the module:: | |
19 | |
20 from wx.lib.pubsub import Publisher | |
21 Publisher().subscribe(...) | |
22 Publisher().sendMessage(...) | |
23 ... | |
24 | |
25 :Author: Oliver Schoenborn | |
26 :Since: Apr 2004 | |
27 :Version: $Id: pubsub.py,v 1.8 2006/06/11 00:12:59 RD Exp $ | |
28 :Copyright: \(c) 2004 Oliver Schoenborn | |
29 :License: wxWidgets | |
30 """ | |
31 | |
32 _implNotes = """ | |
33 Implementation notes | |
34 -------------------- | |
35 | |
36 In class Publisher, I represent the topics-listener set as a tree | |
37 where each node is a topic, and contains a list of listeners of that | |
38 topic, and a dictionary of subtopics of that topic. When the Publisher | |
39 is told to send a message for a given topic, it traverses the tree | |
40 down to the topic for which a message is being generated, all | |
41 listeners on the way get sent the message. | |
42 | |
43 Publisher currently uses a weak listener topic tree to store the | |
44 topics for each listener, and if a listener dies before being | |
45 unsubscribed, the tree is notified, and the tree eliminates the | |
46 listener from itself. | |
47 | |
48 Ideally, _TopicTreeNode would be a generic _TreeNode with named | |
49 subnodes, and _TopicTreeRoot would be a generic _Tree with named | |
50 nodes, and Publisher would store listeners in each node and a topic | |
51 tuple would be converted to a path in the tree. This would lead to a | |
52 much cleaner separation of concerns. But time is over, time to move on. | |
53 """ | |
54 #--------------------------------------------------------------------------- | |
55 | |
56 # for function and method parameter counting: | |
57 from types import InstanceType | |
58 from inspect import getargspec, ismethod, isfunction | |
59 # for weakly bound methods: | |
60 from new import instancemethod as InstanceMethod | |
61 from weakref import ref as WeakRef | |
62 | |
63 # ----------------------------------------------------------------------------- | |
64 | |
65 def _isbound(method): | |
66 """Return true if method is a bound method, false otherwise""" | |
67 assert ismethod(method) | |
68 return method.im_self is not None | |
69 | |
70 | |
71 def _paramMinCountFunc(function): | |
72 """Given a function, return pair (min,d) where min is minimum # of | |
73 args required, and d is number of default arguments.""" | |
74 assert isfunction(function) | |
75 (args, va, kwa, dflt) = getargspec(function) | |
76 lenDef = len(dflt or ()) | |
77 lenArgs = len(args or ()) | |
78 lenVA = int(va is not None) | |
79 return (lenArgs - lenDef + lenVA, lenDef) | |
80 | |
81 | |
82 def _paramMinCount(callableObject): | |
83 """ | |
84 Given a callable object (function, method or callable instance), | |
85 return pair (min,d) where min is minimum # of args required, and d | |
86 is number of default arguments. The 'self' parameter, in the case | |
87 of methods, is not counted. | |
88 """ | |
89 if type(callableObject) is InstanceType: | |
90 min, d = _paramMinCountFunc(callableObject.__call__.im_func) | |
91 return min-1, d | |
92 elif ismethod(callableObject): | |
93 min, d = _paramMinCountFunc(callableObject.im_func) | |
94 return min-1, d | |
95 elif isfunction(callableObject): | |
96 return _paramMinCountFunc(callableObject) | |
97 else: | |
98 raise 'Cannot determine type of callable: '+repr(callableObject) | |
99 | |
100 | |
101 def _tupleize(items): | |
102 """Convert items to tuple if not already one, | |
103 so items must be a list, tuple or non-sequence""" | |
104 if isinstance(items, list): | |
105 raise TypeError, 'Not allowed to tuple-ize a list' | |
106 elif isinstance(items, (str, unicode)) and items.find('.') != -1: | |
107 items = tuple(items.split('.')) | |
108 elif not isinstance(items, tuple): | |
109 items = (items,) | |
110 return items | |
111 | |
112 | |
113 def _getCallableName(callable): | |
114 """Get name for a callable, ie function, bound | |
115 method or callable instance""" | |
116 if ismethod(callable): | |
117 return '%s.%s ' % (callable.im_self, callable.im_func.func_name) | |
118 elif isfunction(callable): | |
119 return '%s ' % callable.__name__ | |
120 else: | |
121 return '%s ' % callable | |
122 | |
123 | |
124 def _removeItem(item, fromList): | |
125 """Attempt to remove item from fromList, return true | |
126 if successful, false otherwise.""" | |
127 try: | |
128 fromList.remove(item) | |
129 return True | |
130 except ValueError: | |
131 return False | |
132 | |
133 | |
134 # ----------------------------------------------------------------------------- | |
135 | |
136 class _WeakMethod: | |
137 """Represent a weak bound method, i.e. a method doesn't keep alive the | |
138 object that it is bound to. It uses WeakRef which, used on its own, | |
139 produces weak methods that are dead on creation, not very useful. | |
140 Typically, you will use the getRef() function instead of using | |
141 this class directly. """ | |
142 | |
143 def __init__(self, method, notifyDead = None): | |
144 """The method must be bound. notifyDead will be called when | |
145 object that method is bound to dies. """ | |
146 assert ismethod(method) | |
147 if method.im_self is None: | |
148 raise ValueError, "We need a bound method!" | |
149 if notifyDead is None: | |
150 self.objRef = WeakRef(method.im_self) | |
151 else: | |
152 self.objRef = WeakRef(method.im_self, notifyDead) | |
153 self.fun = method.im_func | |
154 self.cls = method.im_class | |
155 | |
156 def __call__(self): | |
157 """Returns a new.instancemethod if object for method still alive. | |
158 Otherwise return None. Note that instancemethod causes a | |
159 strong reference to object to be created, so shouldn't save | |
160 the return value of this call. Note also that this __call__ | |
161 is required only for compatibility with WeakRef.ref(), otherwise | |
162 there would be more efficient ways of providing this functionality.""" | |
163 if self.objRef() is None: | |
164 return None | |
165 else: | |
166 return InstanceMethod(self.fun, self.objRef(), self.cls) | |
167 | |
168 def __eq__(self, method2): | |
169 """Two WeakMethod objects compare equal if they refer to the same method | |
170 of the same instance. Thanks to Josiah Carlson for patch and clarifications | |
171 on how dict uses eq/cmp and hashing. """ | |
172 if not isinstance(method2, _WeakMethod): | |
173 return False | |
174 return self.fun is method2.fun \ | |
175 and self.objRef() is method2.objRef() \ | |
176 and self.objRef() is not None | |
177 | |
178 def __hash__(self): | |
179 """Hash is an optimization for dict searches, it need not | |
180 return different numbers for every different object. Some objects | |
181 are not hashable (eg objects of classes derived from dict) so no | |
182 hash(objRef()) in there, and hash(self.cls) would only be useful | |
183 in the rare case where instance method was rebound. """ | |
184 return hash(self.fun) | |
185 | |
186 def __repr__(self): | |
187 dead = '' | |
188 if self.objRef() is None: | |
189 dead = '; DEAD' | |
190 obj = '<%s at %s%s>' % (self.__class__, id(self), dead) | |
191 return obj | |
192 | |
193 def refs(self, weakRef): | |
194 """Return true if we are storing same object referred to by weakRef.""" | |
195 return self.objRef == weakRef | |
196 | |
197 | |
198 def _getWeakRef(obj, notifyDead=None): | |
199 """Get a weak reference to obj. If obj is a bound method, a _WeakMethod | |
200 object, that behaves like a WeakRef, is returned, if it is | |
201 anything else a WeakRef is returned. If obj is an unbound method, | |
202 a ValueError will be raised.""" | |
203 if ismethod(obj): | |
204 createRef = _WeakMethod | |
205 else: | |
206 createRef = WeakRef | |
207 | |
208 if notifyDead is None: | |
209 return createRef(obj) | |
210 else: | |
211 return createRef(obj, notifyDead) | |
212 | |
213 | |
214 # ----------------------------------------------------------------------------- | |
215 | |
216 def getStrAllTopics(): | |
217 """Function to call if, for whatever reason, you need to know | |
218 explicitely what is the string to use to indicate 'all topics'.""" | |
219 return '' | |
220 | |
221 | |
222 # alias, easier to see where used | |
223 ALL_TOPICS = getStrAllTopics() | |
224 | |
225 # ----------------------------------------------------------------------------- | |
226 | |
227 | |
228 class _NodeCallback: | |
229 """Encapsulate a weak reference to a method of a TopicTreeNode | |
230 in such a way that the method can be called, if the node is | |
231 still alive, but the callback does not *keep* the node alive. | |
232 Also, define two methods, preNotify() and noNotify(), which can | |
233 be redefined to something else, very useful for testing. | |
234 """ | |
235 | |
236 def __init__(self, obj): | |
237 self.objRef = _getWeakRef(obj) | |
238 | |
239 def __call__(self, weakCB): | |
240 notify = self.objRef() | |
241 if notify is not None: | |
242 self.preNotify(weakCB) | |
243 notify(weakCB) | |
244 else: | |
245 self.noNotify() | |
246 | |
247 def preNotify(self, dead): | |
248 """'Gets called just before our callback (self.objRef) is called""" | |
249 pass | |
250 | |
251 def noNotify(self): | |
252 """Gets called if the TopicTreeNode for this callback is dead""" | |
253 pass | |
254 | |
255 | |
256 class _TopicTreeNode: | |
257 """A node in the topic tree. This contains a list of callables | |
258 that are interested in the topic that this node is associated | |
259 with, and contains a dictionary of subtopics, whose associated | |
260 values are other _TopicTreeNodes. The topic of a node is not stored | |
261 in the node, so that the tree can be implemented as a dictionary | |
262 rather than a list, for ease of use (and, likely, performance). | |
263 | |
264 Note that it uses _NodeCallback to encapsulate a callback for | |
265 when a registered listener dies, possible thanks to WeakRef. | |
266 Whenever this callback is called, the onDeadListener() function, | |
267 passed in at construction time, is called (unless it is None). | |
268 """ | |
269 | |
270 def __init__(self, topicPath, onDeadListenerWeakCB): | |
271 self.__subtopics = {} | |
272 self.__callables = [] | |
273 self.__topicPath = topicPath | |
274 self.__onDeadListenerWeakCB = onDeadListenerWeakCB | |
275 | |
276 def getPathname(self): | |
277 """The complete node path to us, ie., the topic tuple that would lead to us""" | |
278 return self.__topicPath | |
279 | |
280 def createSubtopic(self, subtopic, topicPath): | |
281 """Create a child node for subtopic""" | |
282 return self.__subtopics.setdefault(subtopic, | |
283 _TopicTreeNode(topicPath, self.__onDeadListenerWeakCB)) | |
284 | |
285 def hasSubtopic(self, subtopic): | |
286 """Return true only if topic string is one of subtopics of this node""" | |
287 return self.__subtopics.has_key(subtopic) | |
288 | |
289 def getNode(self, subtopic): | |
290 """Return ref to node associated with subtopic""" | |
291 return self.__subtopics[subtopic] | |
292 | |
293 def addCallable(self, callable): | |
294 """Add a callable to list of callables for this topic node""" | |
295 try: | |
296 id = self.__callables.index(_getWeakRef(callable)) | |
297 return self.__callables[id] | |
298 except ValueError: | |
299 wrCall = _getWeakRef(callable, _NodeCallback(self.__notifyDead)) | |
300 self.__callables.append(wrCall) | |
301 return wrCall | |
302 | |
303 def getCallables(self): | |
304 """Get callables associated with this topic node""" | |
305 return [cb() for cb in self.__callables if cb() is not None] | |
306 | |
307 def hasCallable(self, callable): | |
308 """Return true if callable in this node""" | |
309 try: | |
310 self.__callables.index(_getWeakRef(callable)) | |
311 return True | |
312 except ValueError: | |
313 return False | |
314 | |
315 def sendMessage(self, message): | |
316 """Send a message to our callables""" | |
317 deliveryCount = 0 | |
318 for cb in self.__callables: | |
319 listener = cb() | |
320 if listener is not None: | |
321 listener(message) | |
322 deliveryCount += 1 | |
323 return deliveryCount | |
324 | |
325 def removeCallable(self, callable): | |
326 """Remove weak callable from our node (and return True). | |
327 Does nothing if not here (and returns False).""" | |
328 try: | |
329 self.__callables.remove(_getWeakRef(callable)) | |
330 return True | |
331 except ValueError: | |
332 return False | |
333 | |
334 def clearCallables(self): | |
335 """Abandon list of callables to caller. We no longer have | |
336 any callables after this method is called.""" | |
337 tmpList = [cb for cb in self.__callables if cb() is not None] | |
338 self.__callables = [] | |
339 return tmpList | |
340 | |
341 def __notifyDead(self, dead): | |
342 """Gets called when a listener dies, thanks to WeakRef""" | |
343 #print 'TreeNODE', `self`, 'received death certificate for ', dead | |
344 self.__cleanupDead() | |
345 if self.__onDeadListenerWeakCB is not None: | |
346 cb = self.__onDeadListenerWeakCB() | |
347 if cb is not None: | |
348 cb(dead) | |
349 | |
350 def __cleanupDead(self): | |
351 """Remove all dead objects from list of callables""" | |
352 self.__callables = [cb for cb in self.__callables if cb() is not None] | |
353 | |
354 def __str__(self): | |
355 """Print us in a not-so-friendly, but readable way, good for debugging.""" | |
356 strVal = [] | |
357 for callable in self.getCallables(): | |
358 strVal.append(_getCallableName(callable)) | |
359 for topic, node in self.__subtopics.iteritems(): | |
360 strVal.append(' (%s: %s)' %(topic, node)) | |
361 return ''.join(strVal) | |
362 | |
363 | |
364 class _TopicTreeRoot(_TopicTreeNode): | |
365 """ | |
366 The root of the tree knows how to access other node of the | |
367 tree and is the gateway of the tree user to the tree nodes. | |
368 It can create topics, and and remove callbacks, etc. | |
369 | |
370 For efficiency, it stores a dictionary of listener-topics, | |
371 so that unsubscribing a listener just requires finding the | |
372 topics associated to a listener, and finding the corresponding | |
373 nodes of the tree. Without it, unsubscribing would require | |
374 that we search the whole tree for all nodes that contain | |
375 given listener. Since Publisher is a singleton, it will | |
376 contain all topics in the system so it is likely to be a large | |
377 tree. However, it is possible that in some runs, unsubscribe() | |
378 is called very little by the user, in which case most unsubscriptions | |
379 are automatic, ie caused by the listeners dying. In this case, | |
380 a flag is set to indicate that the dictionary should be cleaned up | |
381 at the next opportunity. This is not necessary, it is just an | |
382 optimization. | |
383 """ | |
384 | |
385 def __init__(self): | |
386 self.__callbackDict = {} | |
387 self.__callbackDictCleanup = 0 | |
388 # all child nodes will call our __rootNotifyDead method | |
389 # when one of their registered listeners dies | |
390 _TopicTreeNode.__init__(self, (ALL_TOPICS,), | |
391 _getWeakRef(self.__rootNotifyDead)) | |
392 | |
393 def addTopic(self, topic, listener): | |
394 """Add topic to tree if doesnt exist, and add listener to topic node""" | |
395 assert isinstance(topic, tuple) | |
396 topicNode = self.__getTreeNode(topic, make=True) | |
397 weakCB = topicNode.addCallable(listener) | |
398 assert topicNode.hasCallable(listener) | |
399 | |
400 theList = self.__callbackDict.setdefault(weakCB, []) | |
401 assert self.__callbackDict.has_key(weakCB) | |
402 # add it only if we don't already have it | |
403 try: | |
404 weakTopicNode = WeakRef(topicNode) | |
405 theList.index(weakTopicNode) | |
406 except ValueError: | |
407 theList.append(weakTopicNode) | |
408 assert self.__callbackDict[weakCB].index(weakTopicNode) >= 0 | |
409 | |
410 def getTopics(self, listener): | |
411 """Return the list of topics for given listener""" | |
412 weakNodes = self.__callbackDict.get(_getWeakRef(listener), []) | |
413 return [weakNode().getPathname() for weakNode in weakNodes | |
414 if weakNode() is not None] | |
415 | |
416 def isSubscribed(self, listener, topic=None): | |
417 """Return true if listener is registered for topic specified. | |
418 If no topic specified, return true if subscribed to something. | |
419 Use topic=getStrAllTopics() to determine if a listener will receive | |
420 messages for all topics.""" | |
421 weakCB = _getWeakRef(listener) | |
422 if topic is None: | |
423 return self.__callbackDict.has_key(weakCB) | |
424 else: | |
425 topicPath = _tupleize(topic) | |
426 for weakNode in self.__callbackDict[weakCB]: | |
427 if topicPath == weakNode().getPathname(): | |
428 return True | |
429 return False | |
430 | |
431 def unsubscribe(self, listener, topicList): | |
432 """Remove listener from given list of topics. If topicList | |
433 doesn't have any topics for which listener has subscribed, | |
434 nothing happens.""" | |
435 weakCB = _getWeakRef(listener) | |
436 if not self.__callbackDict.has_key(weakCB): | |
437 return | |
438 | |
439 cbNodes = self.__callbackDict[weakCB] | |
440 if topicList is None: | |
441 for weakNode in cbNodes: | |
442 weakNode().removeCallable(listener) | |
443 del self.__callbackDict[weakCB] | |
444 return | |
445 | |
446 for weakNode in cbNodes: | |
447 node = weakNode() | |
448 if node is not None and node.getPathname() in topicList: | |
449 success = node.removeCallable(listener) | |
450 assert success == True | |
451 cbNodes.remove(weakNode) | |
452 assert not self.isSubscribed(listener, node.getPathname()) | |
453 | |
454 def unsubAll(self, topicList, onNoSuchTopic): | |
455 """Unsubscribe all listeners registered for any topic in | |
456 topicList. If a topic in the list does not exist, and | |
457 onNoSuchTopic is not None, a call | |
458 to onNoSuchTopic(topic) is done for that topic.""" | |
459 for topic in topicList: | |
460 node = self.__getTreeNode(topic) | |
461 if node is not None: | |
462 weakCallables = node.clearCallables() | |
463 for callable in weakCallables: | |
464 weakNodes = self.__callbackDict[callable] | |
465 success = _removeItem(WeakRef(node), weakNodes) | |
466 assert success == True | |
467 if weakNodes == []: | |
468 del self.__callbackDict[callable] | |
469 elif onNoSuchTopic is not None: | |
470 onNoSuchTopic(topic) | |
471 | |
472 def sendMessage(self, topic, message, onTopicNeverCreated): | |
473 """Send a message for given topic to all registered listeners. If | |
474 topic doesn't exist, call onTopicNeverCreated(topic).""" | |
475 # send to the all-toipcs listeners | |
476 deliveryCount = _TopicTreeNode.sendMessage(self, message) | |
477 # send to those who listen to given topic or any of its supertopics | |
478 node = self | |
479 for topicItem in topic: | |
480 assert topicItem != '' | |
481 if node.hasSubtopic(topicItem): | |
482 node = node.getNode(topicItem) | |
483 deliveryCount += node.sendMessage(message) | |
484 else: # topic never created, don't bother continuing | |
485 if onTopicNeverCreated is not None: | |
486 onTopicNeverCreated(topic) | |
487 break | |
488 return deliveryCount | |
489 | |
490 def numListeners(self): | |
491 """Return a pair (live, dead) with count of live and dead listeners in tree""" | |
492 dead, live = 0, 0 | |
493 for cb in self.__callbackDict: | |
494 if cb() is None: | |
495 dead += 1 | |
496 else: | |
497 live += 1 | |
498 return live, dead | |
499 | |
500 # clean up the callback dictionary after how many dead listeners | |
501 callbackDeadLimit = 10 | |
502 | |
503 def __rootNotifyDead(self, dead): | |
504 #print 'TreeROOT received death certificate for ', dead | |
505 self.__callbackDictCleanup += 1 | |
506 if self.__callbackDictCleanup > _TopicTreeRoot.callbackDeadLimit: | |
507 self.__callbackDictCleanup = 0 | |
508 oldDict = self.__callbackDict | |
509 self.__callbackDict = {} | |
510 for weakCB, weakNodes in oldDict.iteritems(): | |
511 if weakCB() is not None: | |
512 self.__callbackDict[weakCB] = weakNodes | |
513 | |
514 def __getTreeNode(self, topic, make=False): | |
515 """Return the tree node for 'topic' from the topic tree. If it | |
516 doesnt exist and make=True, create it first.""" | |
517 # if the all-topics, give root; | |
518 if topic == (ALL_TOPICS,): | |
519 return self | |
520 | |
521 # not root, so traverse tree | |
522 node = self | |
523 path = () | |
524 for topicItem in topic: | |
525 path += (topicItem,) | |
526 if topicItem == ALL_TOPICS: | |
527 raise ValueError, 'Topic tuple must not contain ""' | |
528 if make: | |
529 node = node.createSubtopic(topicItem, path) | |
530 elif node.hasSubtopic(topicItem): | |
531 node = node.getNode(topicItem) | |
532 else: | |
533 return None | |
534 # done | |
535 return node | |
536 | |
537 def printCallbacks(self): | |
538 strVal = ['Callbacks:\n'] | |
539 for listener, weakTopicNodes in self.__callbackDict.iteritems(): | |
540 topics = [topic() for topic in weakTopicNodes if topic() is not None] | |
541 strVal.append(' %s: %s\n' % (_getCallableName(listener()), topics)) | |
542 return ''.join(strVal) | |
543 | |
544 def __str__(self): | |
545 return 'all: %s' % _TopicTreeNode.__str__(self) | |
546 | |
547 | |
548 # ----------------------------------------------------------------------------- | |
549 | |
550 class _SingletonKey: pass | |
551 | |
552 class PublisherClass: | |
553 """ | |
554 The publish/subscribe manager. It keeps track of which listeners | |
555 are interested in which topics (see subscribe()), and sends a | |
556 Message for a given topic to listeners that have subscribed to | |
557 that topic, with optional user data (see sendMessage()). | |
558 | |
559 The three important concepts for Publisher are: | |
560 | |
561 - listener: a function, bound method or | |
562 callable object that can be called with one parameter | |
563 (not counting 'self' in the case of methods). The parameter | |
564 will be a reference to a Message object. E.g., these listeners | |
565 are ok:: | |
566 | |
567 class Foo: | |
568 def __call__(self, a, b=1): pass # can be called with only one arg | |
569 def meth(self, a): pass # takes only one arg | |
570 def meth2(self, a=2, b=''): pass # can be called with one arg | |
571 | |
572 def func(a, b=''): pass | |
573 | |
574 Foo foo | |
575 Publisher().subscribe(foo) # functor | |
576 Publisher().subscribe(foo.meth) # bound method | |
577 Publisher().subscribe(foo.meth2) # bound method | |
578 Publisher().subscribe(func) # function | |
579 | |
580 The three types of callables all have arguments that allow a call | |
581 with only one argument. In every case, the parameter 'a' will contain | |
582 the message. | |
583 | |
584 - topic: a single word, a tuple of words, or a string containing a | |
585 set of words separated by dots, for example: 'sports.baseball'. | |
586 A tuple or a dotted notation string denotes a hierarchy of | |
587 topics from most general to least. For example, a listener of | |
588 this topic:: | |
589 | |
590 ('sports','baseball') | |
591 | |
592 would receive messages for these topics:: | |
593 | |
594 ('sports', 'baseball') # because same | |
595 ('sports', 'baseball', 'highscores') # because more specific | |
596 | |
597 but not these:: | |
598 | |
599 'sports' # because more general | |
600 ('sports',) # because more general | |
601 () or ('') # because only for those listening to 'all' topics | |
602 ('news') # because different topic | |
603 | |
604 - message: this is an instance of Message, containing the topic for | |
605 which the message was sent, and any data the sender specified. | |
606 | |
607 :note: This class is visible to importers of pubsub only as a | |
608 Singleton. I.e., every time you execute 'Publisher()', it's | |
609 actually the same instance of PublisherClass that is | |
610 returned. So to use, just do'Publisher().method()'. | |
611 | |
612 """ | |
613 | |
614 __ALL_TOPICS_TPL = (ALL_TOPICS, ) | |
615 | |
616 def __init__(self, singletonKey): | |
617 """Construct a Publisher. This can only be done by the pubsub | |
618 module. You just use pubsub.Publisher().""" | |
619 if not isinstance(singletonKey, _SingletonKey): | |
620 raise invalid_argument("Use Publisher() to get access to singleton") | |
621 self.__messageCount = 0 | |
622 self.__deliveryCount = 0 | |
623 self.__topicTree = _TopicTreeRoot() | |
624 | |
625 # | |
626 # Public API | |
627 # | |
628 | |
629 def getDeliveryCount(self): | |
630 """How many listeners have received a message since beginning of run""" | |
631 return self.__deliveryCount | |
632 | |
633 def getMessageCount(self): | |
634 """How many times sendMessage() was called since beginning of run""" | |
635 return self.__messageCount | |
636 | |
637 def subscribe(self, listener, topic = ALL_TOPICS): | |
638 """ | |
639 Subscribe listener for given topic. If topic is not specified, | |
640 listener will be subscribed for all topics (that listener will | |
641 receive a Message for any topic for which a message is generated). | |
642 | |
643 This method may be called multiple times for one listener, | |
644 registering it with many topics. It can also be invoked many | |
645 times for a particular topic, each time with a different | |
646 listener. See the class doc for requirements on listener and | |
647 topic. | |
648 | |
649 :note: The listener is held by Publisher() only by *weak* | |
650 reference. This means you must ensure you have at | |
651 least one strong reference to listener, otherwise it | |
652 will be DOA ("dead on arrival"). This is particularly | |
653 easy to forget when wrapping a listener method in a | |
654 proxy object (e.g. to bind some of its parameters), | |
655 e.g.:: | |
656 | |
657 class Foo: | |
658 def listener(self, event): pass | |
659 class Wrapper: | |
660 def __init__(self, fun): self.fun = fun | |
661 def __call__(self, *args): self.fun(*args) | |
662 foo = Foo() | |
663 Publisher().subscribe( Wrapper(foo.listener) ) # whoops: DOA! | |
664 wrapper = Wrapper(foo.listener) | |
665 Publisher().subscribe(wrapper) # good! | |
666 | |
667 :note: Calling this method for the same listener, with two | |
668 topics in the same branch of the topic hierarchy, will | |
669 cause the listener to be notified twice when a message | |
670 for the deepest topic is sent. E.g. | |
671 subscribe(listener, 't1') and then subscribe(listener, | |
672 ('t1','t2')) means that when calling sendMessage('t1'), | |
673 listener gets one message, but when calling | |
674 sendMessage(('t1','t2')), listener gets message twice. | |
675 | |
676 """ | |
677 self.validate(listener) | |
678 | |
679 if topic is None: | |
680 raise TypeError, 'Topic must be either a word, tuple of '\ | |
681 'words, or getStrAllTopics()' | |
682 | |
683 self.__topicTree.addTopic(_tupleize(topic), listener) | |
684 | |
685 def isSubscribed(self, listener, topic=None): | |
686 """Return true if listener has subscribed to topic specified. | |
687 If no topic specified, return true if subscribed to something. | |
688 Use topic=getStrAllTopics() to determine if a listener will receive | |
689 messages for all topics.""" | |
690 return self.__topicTree.isSubscribed(listener, topic) | |
691 | |
692 def validate(self, listener): | |
693 """Similar to isValid(), but raises a TypeError exception if not valid""" | |
694 # check callable | |
695 if not callable(listener): | |
696 raise TypeError, 'Listener '+`listener`+' must be a '\ | |
697 'function, bound method or instance.' | |
698 # ok, callable, but if method, is it bound: | |
699 elif ismethod(listener) and not _isbound(listener): | |
700 raise TypeError, 'Listener '+`listener`+\ | |
701 ' is a method but it is unbound!' | |
702 | |
703 # check that it takes the right number of parameters | |
704 min, d = _paramMinCount(listener) | |
705 if min > 1: | |
706 raise TypeError, 'Listener '+`listener`+" can't"\ | |
707 ' require more than one parameter!' | |
708 if min <= 0 and d == 0: | |
709 raise TypeError, 'Listener '+`listener`+' lacking arguments!' | |
710 | |
711 assert (min == 0 and d>0) or (min == 1) | |
712 | |
713 def isValid(self, listener): | |
714 """Return true only if listener will be able to subscribe to | |
715 Publisher.""" | |
716 try: | |
717 self.validate(listener) | |
718 return True | |
719 except TypeError: | |
720 return False | |
721 | |
722 def unsubAll(self, topics=None, onNoSuchTopic=None): | |
723 """Unsubscribe all listeners subscribed for topics. Topics can | |
724 be a single topic (string or tuple) or a list of topics (ie | |
725 list containing strings and/or tuples). If topics is not | |
726 specified, all listeners for all topics will be unsubscribed, | |
727 ie. the Publisher singleton will have no topics and no listeners | |
728 left. If onNoSuchTopic is given, it will be called as | |
729 onNoSuchTopic(topic) for each topic that is unknown. | |
730 """ | |
731 if topics is None: | |
732 del self.__topicTree | |
733 self.__topicTree = _TopicTreeRoot() | |
734 return | |
735 | |
736 # make sure every topics are in tuple form | |
737 if isinstance(topics, list): | |
738 topicList = [_tupleize(x) for x in topics] | |
739 else: | |
740 topicList = [_tupleize(topics)] | |
741 | |
742 # unsub every listener of topics | |
743 self.__topicTree.unsubAll(topicList, onNoSuchTopic) | |
744 | |
745 def unsubscribe(self, listener, topics=None): | |
746 """Unsubscribe listener. If topics not specified, listener is | |
747 completely unsubscribed. Otherwise, it is unsubscribed only | |
748 for the topic (the usual tuple) or list of topics (ie a list | |
749 of tuples) specified. Nothing happens if listener is not actually | |
750 subscribed to any of the topics. | |
751 | |
752 Note that if listener subscribed for two topics (a,b) and (a,c), | |
753 then unsubscribing for topic (a) will do nothing. You must | |
754 use getAssociatedTopics(listener) and give unsubscribe() the returned | |
755 list (or a subset thereof). | |
756 """ | |
757 self.validate(listener) | |
758 topicList = None | |
759 if topics is not None: | |
760 if isinstance(topics, list): | |
761 topicList = [_tupleize(x) for x in topics] | |
762 else: | |
763 topicList = [_tupleize(topics)] | |
764 | |
765 self.__topicTree.unsubscribe(listener, topicList) | |
766 | |
767 def getAssociatedTopics(self, listener): | |
768 """Return a list of topics the given listener is registered with. | |
769 Returns [] if listener never subscribed. | |
770 | |
771 :attention: when using the return of this method to compare to | |
772 expected list of topics, remember that topics that are | |
773 not in the form of a tuple appear as a one-tuple in | |
774 the return. E.g. if you have subscribed a listener to | |
775 'topic1' and ('topic2','subtopic2'), this method | |
776 returns:: | |
777 | |
778 associatedTopics = [('topic1',), ('topic2','subtopic2')] | |
779 """ | |
780 return self.__topicTree.getTopics(listener) | |
781 | |
782 def sendMessage(self, topic=ALL_TOPICS, data=None, onTopicNeverCreated=None): | |
783 """Send a message for given topic, with optional data, to | |
784 subscribed listeners. If topic is not specified, only the | |
785 listeners that are interested in all topics will receive message. | |
786 The onTopicNeverCreated is an optional callback of your choice that | |
787 will be called if the topic given was never created (i.e. it, or | |
788 one of its subtopics, was never subscribed to by any listener). | |
789 It will be called as onTopicNeverCreated(topic).""" | |
790 aTopic = _tupleize(topic) | |
791 message = Message(aTopic, data) | |
792 self.__messageCount += 1 | |
793 | |
794 # send to those who listen to all topics | |
795 self.__deliveryCount += \ | |
796 self.__topicTree.sendMessage(aTopic, message, onTopicNeverCreated) | |
797 | |
798 # | |
799 # Private methods | |
800 # | |
801 | |
802 def __call__(self): | |
803 """Allows for singleton""" | |
804 return self | |
805 | |
806 def __str__(self): | |
807 return str(self.__topicTree) | |
808 | |
809 # Create the Publisher singleton. We prevent users from (inadvertently) | |
810 # instantiating more than one object, by requiring a key that is | |
811 # accessible only to module. From | |
812 # this point forward any calls to Publisher() will invoke the __call__ | |
813 # of this instance which just returns itself. | |
814 # | |
815 # The only flaw with this approach is that you can't derive a new | |
816 # class from Publisher without jumping through hoops. If this ever | |
817 # becomes an issue then a new Singleton implementaion will need to be | |
818 # employed. | |
819 _key = _SingletonKey() | |
820 Publisher = PublisherClass(_key) | |
821 | |
822 | |
823 #--------------------------------------------------------------------------- | |
824 | |
825 class Message: | |
826 """ | |
827 A simple container object for the two components of a message: the | |
828 topic and the user data. An instance of Message is given to your | |
829 listener when called by Publisher().sendMessage(topic) (if your | |
830 listener callback was registered for that topic). | |
831 """ | |
832 def __init__(self, topic, data): | |
833 self.topic = topic | |
834 self.data = data | |
835 | |
836 def __str__(self): | |
837 return '[Topic: '+`self.topic`+', Data: '+`self.data`+']' |