]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/pubsub.py
   1 #--------------------------------------------------------------------------- 
   2 # Name:        wxPython.lib.pubsub 
   3 # Purpose:     The Publish/Subscribe framework used by evtmgr.EventManager 
   5 # Author:      Robb Shecter and Robin Dunn 
   7 # Created:     12-December-2002 
   9 # Copyright:   (c) 2002 by db-X Corporation 
  10 # Licence:     wxWindows license 
  11 #--------------------------------------------------------------------------- 
  13 This module has classes for implementing the Publish/Subscribe design 
  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 
  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 
  28 The Server and Message classes are the two that clients interact 
  31 This module is compatible with Python 2.1. 
  36 #--------------------------------------------------------------------------- 
  40     The publish/subscribe server.  This class is a Singleton. 
  44         self
.functionDict      
= {} 
  45         self
.subscribeAllList  
= [] 
  47         self
.deliveryCount     
= 0 
  54     def subscribe(self
, topic
, listener
): 
  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 
  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. 
  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: 
  78           ('sports', 'baseball') 
  79           ('sports', 'baseball', 'highscores') 
  87         if not callable(listener
): 
  88             raise TypeError('The P/S listener, '+`listener`
+', is not callable.') 
  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
) 
  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
) 
 100         # Add to a dict in order to speed-up unsubscribing. 
 101         self
.__addFunctionLookup
(listener
, aTopic
) 
 104     def unsubscribe(self
, listener
): 
 106         Remove the given listener from the registry, 
 107         for all topics that it's associated with. 
 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
) 
 115             for subscriber 
in subscriberList
: 
 116                 if subscriber
[0] != listener
: 
 117                     listToKeep
.append(subscriber
) 
 118             self
.__setTopicList
(aTopic
, listToKeep
) 
 119         self
.__delFunctionLookup
(listener
) 
 122     def getAssociatedTopics(self
, listener
): 
 124         Return a list of topics the given listener is 
 127         return self
.functionDict
.get(listener
, []) 
 130     def sendMessage(self
, topic
, data
=None): 
 132         Relay a message to registered listeners. 
 134         aTopic    
= Topic(topic
) 
 135         message   
= Message(aTopic
.items
, data
) 
 136         topicList 
= self
.__getTopicList
(aTopic
) 
 138         # Send to the matching topics 
 139         for subscriber 
in topicList
: 
 140             if subscriber
[1].matches(aTopic
): 
 141                 subscriber
[2](message
) 
 143         # Send to any listeners registered for ALL 
 144         for subscriber 
in self
.subscribeAllList
: 
 145             subscriber
[2](message
) 
 152     def __makeCallable(self
, function
): 
 154         Return a function that is what the server 
 157         This is a time optimization: this removes a test 
 158         for the number of parameters from the inner loop 
 161         parameters 
= self
.__parameterCount
(function
) 
 163             # Return a function that calls the listener 
 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
) 
 171             raise TypeError('The publish/subscribe listener, '+`function`
+', has wrong parameter count') 
 174     def __parameterCount(self
, callableObject
): 
 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. 
 181             # Try to handle this like a method 
 182             return callableObject
.im_func
.func_code
.co_argcount 
- 1 
 183         except AttributeError: 
 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
) 
 192     def __addFunctionLookup(self
, aFunction
, aTopic
): 
 194             aList 
= self
.functionDict
[aFunction
] 
 197             self
.functionDict
[aFunction
] = aList
 
 201     def __delFunctionLookup(self
, aFunction
): 
 203             del self
.functionDict
[aFunction
] 
 205             print 'Warning: listener not found. Logic error in PublishSubscribe?', aFunction
 
 208     def __addTopicToCorrectList(self
, topic
, listener
, callableVersion
): 
 209         if len(topic
.items
) == 0: 
 210             self
.subscribeAllList
.append((listener
, topic
, callableVersion
)) 
 212             self
.__getTopicList
(topic
).append((listener
, topic
, callableVersion
)) 
 215     def __getTopicList(self
, aTopic
): 
 217         Return the correct sublist of subscribers based on the 
 221             elementZero 
= aTopic
.items
[0] 
 223             return self
.subscribeAllList
 
 226             subList 
= self
.topicDict
[elementZero
] 
 229             self
.topicDict
[elementZero
] = subList
 
 233     def __setTopicList(self
, aTopic
, aSubscriberList
): 
 235             self
.topicDict
[aTopic
.items
[0]] = aSubscriberList
 
 237             self
.subscribeAllList 
= aSubscriberList
 
 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. 
 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 
 253 Publisher 
= Publisher() 
 256 #--------------------------------------------------------------------------- 
 260     A simple container object for the two components of 
 261     a message; the topic and the data. 
 263     def __init__(self
, topic
, data
): 
 268         return '[Topic: '+`self
.topic`
+',  Data: '+`self
.data`
+']' 
 271 #--------------------------------------------------------------------------- 
 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. 
 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 
 289     def __init__(self
, items
): 
 290         # Make sure we have a tuple. 
 291         if type(items
) == self
.__class
__.listType
: 
 293         elif type(items
) != self
.__class
__.tupleType
: 
 296         self
.length 
= len(items
) 
 299     def matches(self
, aTopic
): 
 301         Consider myself to be a topic pattern, 
 302         and return True if I match the given specific 
 305         b = ('sports','baseball') 
 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 
 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
]) 
 324         return '<Topic>' + string
.join(map(repr, self
.items
), ', ') + '</Topic>' 
 327     def __eq__(self
, aTopic
): 
 329         Return True if I equal the given topic.  We're considered 
 330         equal if our tuples are equal. 
 332         if type(self
) != type(aTopic
): 
 335             return self
.items 
== aTopic
.items
 
 338     def __ne__(self
, aTopic
): 
 340         Return False if I equal the given topic. 
 342         return not self 
== aTopic
 
 345 #--------------------------------------------------------------------------- 
 349 # Code for a simple command-line test 
 351 if __name__ 
== '__main__': 
 353     class SimpleListener
: 
 354         def __init__(self
, number
): 
 356         def notify(self
, message
): 
 357             print '#'+str(self
.number
)+' got the message:', message
 
 359     # Build a list of ten listeners. 
 362         lList
.append(SimpleListener(x
)) 
 366     # Everyone's interested in politics... 
 368         Publisher().subscribe(topic
='politics', listener
=x
.notify
)  # also tests singleton 
 370     # But only the first four are interested in trivia. 
 372         server
.subscribe(topic
='trivia',   listener
=x
.notify
) 
 374     # This one subscribes to everything. 
 375     everythingListener 
= SimpleListener(999) 
 376     server
.subscribe(topic
=(), listener
=everythingListener
.notify
) 
 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.') 
 382 #---------------------------------------------------------------------------