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