]>
Commit | Line | Data |
---|---|---|
d1e05453 | 1 | |
d14a1e28 RD |
2 | #--------------------------------------------------------------------------- |
3 | """ | |
d1e05453 RD |
4 | This module provides a publish-subscribe component that allows |
5 | listeners to subcribe to messages of a given topic. Contrary to the | |
0cdd86d6 RD |
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. | |
d1e05453 RD |
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 | ||
0cdd86d6 RD |
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 | ||
d1e05453 RD |
25 | :Author: Oliver Schoenborn |
26 | :Since: Apr 2004 | |
27 | :Version: $Id$ | |
28 | :Copyright: \(c) 2004 Oliver Schoenborn | |
29 | :License: wxWidgets | |
0cdd86d6 | 30 | """ |
d1e05453 | 31 | |
0cdd86d6 | 32 | _implNotes = """ |
d1e05453 RD |
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 | |
b0429a40 | 52 | much cleaner separation of concerns. But time is over, time to move on. |
d1e05453 RD |
53 | """ |
54 | #--------------------------------------------------------------------------- | |
1fded56b | 55 | |
d1e05453 RD |
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 | |
d14a1e28 | 62 | |
d1e05453 | 63 | # ----------------------------------------------------------------------------- |
d14a1e28 | 64 | |
d1e05453 RD |
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 | |
d14a1e28 | 69 | |
d14a1e28 | 70 | |
d1e05453 RD |
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 ()) | |
0cdd86d6 RD |
77 | lenArgs = len(args or ()) |
78 | lenVA = int(va is not None) | |
79 | return (lenArgs - lenDef + lenVA, lenDef) | |
d14a1e28 | 80 | |
d1e05453 RD |
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""" | |
b0429a40 | 104 | if isinstance(items, list): |
d1e05453 | 105 | raise TypeError, 'Not allowed to tuple-ize a list' |
b0429a40 RD |
106 | elif isinstance(items, (str, unicode)) and items.find('.') != -1: |
107 | items = tuple(items.split('.')) | |
108 | elif not isinstance(items, tuple): | |
d1e05453 RD |
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 | ||
0cdd86d6 RD |
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 | |
d1e05453 RD |
177 | |
178 | def __hash__(self): | |
0cdd86d6 RD |
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) | |
d1e05453 RD |
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. | |
d14a1e28 | 234 | """ |
d1e05453 RD |
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). | |
d14a1e28 | 268 | """ |
d1e05453 RD |
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 | ||
d14a1e28 | 385 | def __init__(self): |
d1e05453 RD |
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, | |
0cdd86d6 | 434 | nothing happens.""" |
d1e05453 RD |
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: | |
0cdd86d6 | 486 | onTopicNeverCreated(topic) |
d1e05453 RD |
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 | # ----------------------------------------------------------------------------- | |
d14a1e28 | 549 | |
0cdd86d6 RD |
550 | class _SingletonKey: pass |
551 | ||
552 | class PublisherClass: | |
d1e05453 RD |
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 | |
0cdd86d6 | 562 | callable object that can be called with one parameter |
d1e05453 RD |
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 | ||
b0429a40 RD |
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:: | |
d1e05453 RD |
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 | ||
9f4cc34f RD |
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 | ||
d1e05453 RD |
612 | """ |
613 | ||
614 | __ALL_TOPICS_TPL = (ALL_TOPICS, ) | |
615 | ||
0cdd86d6 RD |
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") | |
d1e05453 RD |
621 | self.__messageCount = 0 |
622 | self.__deliveryCount = 0 | |
623 | self.__topicTree = _TopicTreeRoot() | |
d14a1e28 RD |
624 | |
625 | # | |
626 | # Public API | |
627 | # | |
628 | ||
d1e05453 RD |
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): | |
d14a1e28 | 638 | """ |
d1e05453 RD |
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 | ||
b0429a40 RD |
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. | |
d1e05453 | 648 | |
9f4cc34f RD |
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! | |
0cdd86d6 | 666 | |
9f4cc34f RD |
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. | |
0cdd86d6 | 675 | |
d14a1e28 | 676 | """ |
d1e05453 RD |
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. | |
0cdd86d6 | 688 | Use topic=getStrAllTopics() to determine if a listener will receive |
d1e05453 RD |
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 | |
d14a1e28 | 695 | if not callable(listener): |
d1e05453 RD |
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): | |
0cdd86d6 RD |
714 | """Return true only if listener will be able to subscribe to |
715 | Publisher.""" | |
d1e05453 RD |
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 | |
0cdd86d6 RD |
728 | left. If onNoSuchTopic is given, it will be called as |
729 | onNoSuchTopic(topic) for each topic that is unknown. | |
730 | """ | |
d1e05453 RD |
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 | |
b0429a40 | 737 | if isinstance(topics, list): |
d1e05453 RD |
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 | |
0cdd86d6 RD |
749 | of tuples) specified. Nothing happens if listener is not actually |
750 | subscribed to any of the topics. | |
d1e05453 RD |
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). | |
d14a1e28 | 756 | """ |
d1e05453 RD |
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 | ||
d14a1e28 | 767 | def getAssociatedTopics(self, listener): |
d1e05453 RD |
768 | """Return a list of topics the given listener is registered with. |
769 | Returns [] if listener never subscribed. | |
770 | ||
9f4cc34f RD |
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:: | |
d1e05453 | 777 | |
9f4cc34f | 778 | associatedTopics = [('topic1',), ('topic2','subtopic2')] |
d14a1e28 | 779 | """ |
d1e05453 RD |
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 | |
0cdd86d6 RD |
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).""" | |
d1e05453 RD |
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 | ||
d14a1e28 RD |
798 | # |
799 | # Private methods | |
800 | # | |
801 | ||
d14a1e28 | 802 | def __call__(self): |
d1e05453 | 803 | """Allows for singleton""" |
d14a1e28 | 804 | return self |
d1e05453 RD |
805 | |
806 | def __str__(self): | |
807 | return str(self.__topicTree) | |
d14a1e28 | 808 | |
0cdd86d6 RD |
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 | |
d14a1e28 RD |
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. | |
0cdd86d6 RD |
819 | _key = _SingletonKey() |
820 | Publisher = PublisherClass(_key) | |
d14a1e28 RD |
821 | |
822 | ||
823 | #--------------------------------------------------------------------------- | |
824 | ||
825 | class Message: | |
826 | """ | |
0cdd86d6 RD |
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). | |
d14a1e28 RD |
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`+']' | |
838 | ||
839 | ||
d14a1e28 RD |
840 | #--------------------------------------------------------------------------- |
841 | ||
842 | ||
843 | # | |
844 | # Code for a simple command-line test | |
845 | # | |
d1e05453 RD |
846 | def test(): |
847 | def done(funcName): | |
848 | print '----------- Done %s -----------' % funcName | |
849 | ||
850 | def testParam(): | |
0cdd86d6 RD |
851 | def testFunc00(): pass |
852 | def testFunc21(a,b,c=1): pass | |
853 | def testFuncA(*args): pass | |
854 | def testFuncAK(*args,**kwds): pass | |
855 | def testFuncK(**kwds): pass | |
856 | ||
d1e05453 RD |
857 | class Foo: |
858 | def testMeth(self,a,b): pass | |
859 | def __call__(self, a): pass | |
0cdd86d6 RD |
860 | class Foo2: |
861 | def __call__(self, *args): pass | |
d1e05453 | 862 | |
0cdd86d6 RD |
863 | assert _paramMinCount(testFunc00)==(0,0) |
864 | assert _paramMinCount(testFunc21)==(2,1) | |
865 | assert _paramMinCount(testFuncA) ==(1,0) | |
866 | assert _paramMinCount(testFuncAK)==(1,0) | |
867 | assert _paramMinCount(testFuncK) ==(0,0) | |
d1e05453 | 868 | foo = Foo() |
d1e05453 RD |
869 | assert _paramMinCount(Foo.testMeth)==(2,0) |
870 | assert _paramMinCount(foo.testMeth)==(2,0) | |
871 | assert _paramMinCount(foo)==(1,0) | |
0cdd86d6 | 872 | assert _paramMinCount(Foo2())==(1,0) |
d1e05453 RD |
873 | |
874 | done('testParam') | |
875 | ||
876 | testParam() | |
877 | #------------------------ | |
878 | ||
879 | _NodeCallback.notified = 0 | |
880 | def testPreNotifyNode(self, dead): | |
881 | _NodeCallback.notified += 1 | |
882 | print 'testPreNotifyNODE heard notification of', `dead` | |
883 | _NodeCallback.preNotify = testPreNotifyNode | |
884 | ||
885 | def testTreeNode(): | |
886 | ||
887 | class WS: | |
888 | def __init__(self, s): | |
889 | self.s = s | |
890 | def __call__(self, msg): | |
891 | print 'WS#', self.s, ' received msg ', msg | |
892 | def __str__(self): | |
893 | return self.s | |
894 | ||
895 | def testPreNotifyRoot(dead): | |
896 | print 'testPreNotifyROOT heard notification of', `dead` | |
897 | ||
898 | node = _TopicTreeNode((ALL_TOPICS,), WeakRef(testPreNotifyRoot)) | |
899 | boo, baz, bid = WS('boo'), WS('baz'), WS('bid') | |
900 | node.addCallable(boo) | |
901 | node.addCallable(baz) | |
902 | node.addCallable(boo) | |
903 | assert node.getCallables() == [boo,baz] | |
904 | assert node.hasCallable(boo) | |
905 | ||
906 | node.removeCallable(bid) # no-op | |
907 | assert node.hasCallable(baz) | |
908 | assert node.getCallables() == [boo,baz] | |
909 | ||
910 | node.removeCallable(boo) | |
911 | assert node.getCallables() == [baz] | |
912 | assert node.hasCallable(baz) | |
913 | assert not node.hasCallable(boo) | |
914 | ||
915 | node.removeCallable(baz) | |
916 | assert node.getCallables() == [] | |
917 | assert not node.hasCallable(baz) | |
918 | ||
919 | node2 = node.createSubtopic('st1', ('st1',)) | |
920 | node3 = node.createSubtopic('st2', ('st2',)) | |
921 | cb1, cb2, cb = WS('st1_cb1'), WS('st1_cb2'), WS('st2_cb') | |
922 | node2.addCallable(cb1) | |
923 | node2.addCallable(cb2) | |
924 | node3.addCallable(cb) | |
925 | node2.createSubtopic('st3', ('st1','st3')) | |
926 | node2.createSubtopic('st4', ('st1','st4')) | |
927 | ||
928 | print str(node) | |
929 | assert str(node) == ' (st1: st1_cb1 st1_cb2 (st4: ) (st3: )) (st2: st2_cb )' | |
930 | ||
931 | # verify send message, and that a dead listener does not get sent one | |
932 | delivered = node2.sendMessage('hello') | |
933 | assert delivered == 2 | |
934 | del cb1 | |
935 | delivered = node2.sendMessage('hello') | |
936 | assert delivered == 1 | |
937 | assert _NodeCallback.notified == 1 | |
938 | ||
939 | done('testTreeNode') | |
940 | ||
941 | testTreeNode() | |
942 | #------------------------ | |
943 | ||
944 | def testValidate(): | |
945 | class Foo: | |
946 | def __call__(self, a): pass | |
947 | def fun(self, b): pass | |
948 | def fun2(self, b=1): pass | |
949 | def fun3(self, a, b=2): pass | |
950 | def badFun(self): pass | |
951 | def badFun2(): pass | |
952 | def badFun3(self, a, b): pass | |
953 | ||
954 | server = Publisher() | |
955 | foo = Foo() | |
956 | server.validate(foo) | |
957 | server.validate(foo.fun) | |
958 | server.validate(foo.fun2) | |
959 | server.validate(foo.fun3) | |
960 | assert not server.isValid(foo.badFun) | |
961 | assert not server.isValid(foo.badFun2) | |
962 | assert not server.isValid(foo.badFun3) | |
963 | ||
964 | done('testValidate') | |
965 | ||
966 | testValidate() | |
967 | #------------------------ | |
968 | ||
d14a1e28 RD |
969 | class SimpleListener: |
970 | def __init__(self, number): | |
971 | self.number = number | |
d1e05453 RD |
972 | def __call__(self, message = ''): |
973 | print 'Callable #%s got the message "%s"' %(self.number, message) | |
d14a1e28 | 974 | def notify(self, message): |
d1e05453 RD |
975 | print '%s.notify() got the message "%s"' %(self.number, message) |
976 | def __str__(self): | |
977 | return "SimpleListener_%s" % self.number | |
978 | ||
979 | def testSubscribe(): | |
980 | publisher = Publisher() | |
981 | ||
982 | topic1 = 'politics' | |
983 | topic2 = ('history','middle age') | |
984 | topic3 = ('politics','UN') | |
985 | topic4 = ('politics','NATO') | |
986 | topic5 = ('politics','NATO','US') | |
987 | ||
988 | lisnr1 = SimpleListener(1) | |
989 | lisnr2 = SimpleListener(2) | |
990 | def func(message, a=1): | |
991 | print 'Func received message "%s"' % message | |
992 | lisnr3 = func | |
993 | lisnr4 = lambda x: 'Lambda received message "%s"' % x | |
994 | ||
995 | assert not publisher.isSubscribed(lisnr1) | |
996 | assert not publisher.isSubscribed(lisnr2) | |
997 | assert not publisher.isSubscribed(lisnr3) | |
998 | assert not publisher.isSubscribed(lisnr4) | |
999 | ||
1000 | publisher.subscribe(lisnr1, topic1) | |
1001 | assert publisher.getAssociatedTopics(lisnr1) == [(topic1,)] | |
1002 | publisher.subscribe(lisnr1, topic2) | |
1003 | publisher.subscribe(lisnr1, topic1) # do it again, should be no-op | |
1004 | assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2] | |
1005 | publisher.subscribe(lisnr2.notify, topic3) | |
1006 | assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3] | |
1007 | assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2] | |
1008 | publisher.subscribe(lisnr3, topic5) | |
1009 | assert publisher.getAssociatedTopics(lisnr3) == [topic5] | |
1010 | assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3] | |
1011 | assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2] | |
1012 | publisher.subscribe(lisnr4) | |
1013 | ||
1014 | print "Publisher tree: ", publisher | |
1015 | assert publisher.isSubscribed(lisnr1) | |
1016 | assert publisher.isSubscribed(lisnr1, topic1) | |
1017 | assert publisher.isSubscribed(lisnr1, topic2) | |
1018 | assert publisher.isSubscribed(lisnr2.notify) | |
1019 | assert publisher.isSubscribed(lisnr3, topic5) | |
1020 | assert publisher.isSubscribed(lisnr4, ALL_TOPICS) | |
1021 | expectTopicTree = 'all: <lambda> (politics: SimpleListener_1 (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: SimpleListener_1 ))' | |
1022 | print "Publisher tree: ", publisher | |
1023 | assert str(publisher) == expectTopicTree | |
1024 | ||
1025 | publisher.unsubscribe(lisnr1, 'booboo') # should do nothing | |
1026 | assert publisher.getAssociatedTopics(lisnr1) == [(topic1,),topic2] | |
1027 | assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3] | |
1028 | assert publisher.getAssociatedTopics(lisnr3) == [topic5] | |
1029 | publisher.unsubscribe(lisnr1, topic1) | |
1030 | assert publisher.getAssociatedTopics(lisnr1) == [topic2] | |
1031 | assert publisher.getAssociatedTopics(lisnr2.notify) == [topic3] | |
1032 | assert publisher.getAssociatedTopics(lisnr3) == [topic5] | |
1033 | publisher.unsubscribe(lisnr1, topic2) | |
1034 | publisher.unsubscribe(lisnr1, topic2) | |
1035 | publisher.unsubscribe(lisnr2.notify, topic3) | |
1036 | publisher.unsubscribe(lisnr3, topic5) | |
1037 | assert publisher.getAssociatedTopics(lisnr1) == [] | |
1038 | assert publisher.getAssociatedTopics(lisnr2.notify) == [] | |
1039 | assert publisher.getAssociatedTopics(lisnr3) == [] | |
1040 | publisher.unsubscribe(lisnr4) | |
1041 | ||
1042 | expectTopicTree = 'all: (politics: (UN: ) (NATO: (US: ))) (history: (middle age: ))' | |
1043 | print "Publisher tree: ", publisher | |
1044 | assert str(publisher) == expectTopicTree | |
1045 | assert publisher.getDeliveryCount() == 0 | |
1046 | assert publisher.getMessageCount() == 0 | |
1047 | ||
1048 | publisher.unsubAll() | |
1049 | assert str(publisher) == 'all: ' | |
1050 | ||
1051 | done('testSubscribe') | |
1052 | ||
1053 | testSubscribe() | |
1054 | #------------------------ | |
1055 | ||
1056 | def testUnsubAll(): | |
1057 | publisher = Publisher() | |
1058 | ||
1059 | topic1 = 'politics' | |
1060 | topic2 = ('history','middle age') | |
1061 | topic3 = ('politics','UN') | |
1062 | topic4 = ('politics','NATO') | |
1063 | topic5 = ('politics','NATO','US') | |
1064 | ||
1065 | lisnr1 = SimpleListener(1) | |
1066 | lisnr2 = SimpleListener(2) | |
1067 | def func(message, a=1): | |
1068 | print 'Func received message "%s"' % message | |
1069 | lisnr3 = func | |
1070 | lisnr4 = lambda x: 'Lambda received message "%s"' % x | |
1071 | ||
1072 | publisher.subscribe(lisnr1, topic1) | |
1073 | publisher.subscribe(lisnr1, topic2) | |
1074 | publisher.subscribe(lisnr2.notify, topic3) | |
1075 | publisher.subscribe(lisnr3, topic2) | |
1076 | publisher.subscribe(lisnr3, topic5) | |
1077 | publisher.subscribe(lisnr4) | |
1078 | ||
1079 | expectTopicTree = 'all: <lambda> (politics: SimpleListener_1 (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: SimpleListener_1 func ))' | |
1080 | print "Publisher tree: ", publisher | |
1081 | assert str(publisher) == expectTopicTree | |
1082 | ||
1083 | publisher.unsubAll(topic1) | |
1084 | assert publisher.getAssociatedTopics(lisnr1) == [topic2] | |
1085 | assert not publisher.isSubscribed(lisnr1, topic1) | |
1086 | ||
1087 | publisher.unsubAll(topic2) | |
1088 | print publisher | |
1089 | assert publisher.getAssociatedTopics(lisnr1) == [] | |
1090 | assert publisher.getAssociatedTopics(lisnr3) == [topic5] | |
1091 | assert not publisher.isSubscribed(lisnr1) | |
1092 | assert publisher.isSubscribed(lisnr3, topic5) | |
1093 | ||
1094 | #print "Publisher tree: ", publisher | |
1095 | expectTopicTree = 'all: <lambda> (politics: (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: ))' | |
1096 | assert str(publisher) == expectTopicTree | |
1097 | publisher.unsubAll(ALL_TOPICS) | |
1098 | #print "Publisher tree: ", publisher | |
1099 | expectTopicTree = 'all: (politics: (UN: SimpleListener_2.notify ) (NATO: (US: func ))) (history: (middle age: ))' | |
1100 | assert str(publisher) == expectTopicTree | |
1101 | ||
1102 | publisher.unsubAll() | |
1103 | done('testUnsubAll') | |
1104 | ||
1105 | testUnsubAll() | |
1106 | #------------------------ | |
1107 | ||
1108 | def testSend(): | |
1109 | publisher = Publisher() | |
1110 | called = [] | |
1111 | ||
1112 | class TestListener: | |
1113 | def __init__(self, num): | |
1114 | self.number = num | |
1115 | def __call__(self, b): | |
1116 | called.append( 'TL%scb' % self.number ) | |
1117 | def notify(self, b): | |
1118 | called.append( 'TL%sm' % self.number ) | |
1119 | def funcListener(b): | |
1120 | called.append('func') | |
1121 | ||
1122 | lisnr1 = TestListener(1) | |
1123 | lisnr2 = TestListener(2) | |
1124 | lisnr3 = funcListener | |
1125 | lisnr4 = lambda x: called.append('lambda') | |
1126 | ||
1127 | topic1 = 'politics' | |
1128 | topic2 = 'history' | |
1129 | topic3 = ('politics','UN') | |
1130 | topic4 = ('politics','NATO','US') | |
1131 | topic5 = ('politics','NATO') | |
1132 | ||
1133 | publisher.subscribe(lisnr1, topic1) | |
1134 | publisher.subscribe(lisnr2, topic2) | |
1135 | publisher.subscribe(lisnr2.notify, topic2) | |
1136 | publisher.subscribe(lisnr3, topic4) | |
1137 | publisher.subscribe(lisnr4) | |
1138 | ||
1139 | print publisher | |
1140 | ||
1141 | # setup ok, now test send/receipt | |
1142 | publisher.sendMessage(topic1) | |
1143 | assert called == ['lambda','TL1cb'] | |
1144 | called = [] | |
1145 | publisher.sendMessage(topic2) | |
1146 | assert called == ['lambda','TL2cb','TL2m'] | |
1147 | called = [] | |
1148 | publisher.sendMessage(topic3) | |
1149 | assert called == ['lambda','TL1cb'] | |
1150 | called = [] | |
1151 | publisher.sendMessage(topic4) | |
1152 | assert called == ['lambda','TL1cb','func'] | |
1153 | called = [] | |
1154 | publisher.sendMessage(topic5) | |
1155 | assert called == ['lambda','TL1cb'] | |
1156 | assert publisher.getDeliveryCount() == 12 | |
1157 | assert publisher.getMessageCount() == 5 | |
1158 | ||
1159 | # test weak referencing works: | |
1160 | _NodeCallback.notified = 0 | |
1161 | del lisnr2 | |
1162 | called = [] | |
1163 | publisher.sendMessage(topic2) | |
1164 | assert called == ['lambda'] | |
1165 | assert _NodeCallback.notified == 2 | |
1166 | ||
1167 | done('testSend') | |
1168 | ||
1169 | testSend() | |
1170 | assert _NodeCallback.notified == 5 | |
1171 | ||
1172 | def testDead(): | |
1173 | # verify if weak references work as expected | |
1174 | print '------ Starting testDead ----------' | |
1175 | node = _TopicTreeNode('t1', None) | |
1176 | lisnr1 = SimpleListener(1) | |
1177 | lisnr2 = SimpleListener(2) | |
1178 | lisnr3 = SimpleListener(3) | |
1179 | lisnr4 = SimpleListener(4) | |
1180 | ||
1181 | node.addCallable(lisnr1) | |
1182 | node.addCallable(lisnr2) | |
1183 | node.addCallable(lisnr3) | |
1184 | node.addCallable(lisnr4) | |
1185 | ||
1186 | print 'Deleting listeners first' | |
1187 | _NodeCallback.notified = 0 | |
1188 | del lisnr1 | |
1189 | del lisnr2 | |
1190 | assert _NodeCallback.notified == 2 | |
1191 | ||
1192 | print 'Deleting node first' | |
1193 | _NodeCallback.notified = 0 | |
1194 | del node | |
1195 | del lisnr3 | |
1196 | del lisnr4 | |
1197 | assert _NodeCallback.notified == 0 | |
1198 | ||
1199 | lisnr1 = SimpleListener(1) | |
1200 | lisnr2 = SimpleListener(2) | |
1201 | lisnr3 = SimpleListener(3) | |
1202 | lisnr4 = SimpleListener(4) | |
1203 | ||
1204 | # try same with root of tree | |
1205 | node = _TopicTreeRoot() | |
1206 | node.addTopic(('',), lisnr1) | |
1207 | node.addTopic(('',), lisnr2) | |
1208 | node.addTopic(('',), lisnr3) | |
1209 | node.addTopic(('',), lisnr4) | |
1210 | # add objects that will die immediately to see if cleanup occurs | |
1211 | # this must be done visually as it is a low-level detail | |
1212 | _NodeCallback.notified = 0 | |
1213 | _TopicTreeRoot.callbackDeadLimit = 3 | |
1214 | node.addTopic(('',), SimpleListener(5)) | |
1215 | node.addTopic(('',), SimpleListener(6)) | |
1216 | node.addTopic(('',), SimpleListener(7)) | |
1217 | print node.numListeners() | |
1218 | assert node.numListeners() == (4, 3) | |
1219 | node.addTopic(('',), SimpleListener(8)) | |
1220 | assert node.numListeners() == (4, 0) | |
1221 | assert _NodeCallback.notified == 4 | |
1222 | ||
1223 | print 'Deleting listeners first' | |
1224 | _NodeCallback.notified = 0 | |
1225 | del lisnr1 | |
1226 | del lisnr2 | |
1227 | assert _NodeCallback.notified == 2 | |
1228 | print 'Deleting node first' | |
1229 | _NodeCallback.notified = 0 | |
1230 | del node | |
1231 | del lisnr3 | |
1232 | del lisnr4 | |
1233 | assert _NodeCallback.notified == 0 | |
1234 | ||
1235 | done('testDead') | |
1236 | ||
1237 | testDead() | |
1238 | ||
1239 | print 'Exiting tests' | |
d14a1e28 | 1240 | #--------------------------------------------------------------------------- |
d1e05453 RD |
1241 | |
1242 | if __name__ == '__main__': | |
1243 | test() |