1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
|
'''
This module provides publish-subscribe functions that allow
your methods, functions, and any other callable object to subscribe to
messages of a given topic, sent from anywhere in your application.
It therefore provides a powerful decoupling mechanism, e.g. between
GUI and application logic: senders and listeners don't need to know about
each other.
E.g. the following sends a message of type 'MsgType' to a listener,
carrying data 'some data' (in this case, a string, but could be anything)::
import pubsub2 as ps
class MsgType(ps.Message):
pass
def listener(msg, data):
print 'got msg', data
ps.subscribe(listener, MsgType)
ps.sendMessage(MsgType('some data'))
The only requirement on your listener is that it be a callable that takes
the message instance as the first argument, and any args/kwargs come after.
Contrary to pubsub, with pubsub2 the data sent with your message is
specified in the message instance constructor, and those parameters are
passed on directly to your listener via its parameter list.
The important concepts of pubsub2 are:
- topic: the message type. This is a 'dotted' sequence of class names,
defined in your messaging module e.g. yourmsgs.py. The sequence
denotes a hierarchy of topics from most general to least.
For example, a listener of this topic::
Sports.Baseball
would receive messages for these topics::
Sports.Baseball # because same
Sports.Baseball.Highscores # because more specific
but not these::
Sports # because more general
News # because different topic
Defining a topic hierarchy is trivial: in yourmsgs.py you would do e.g.::
import pubsub2 as ps
class Sports(ps.Message):
class Baseball(ps.Message):
class Highscores(ps.Message): pass
class Lowscores(ps.Message): pass
class Hockey(ps.Message):
class Highscores(ps.Message): pass
ps.setupMsgTree(Sports) # don't forget this!
Note that the above allows you to document your message topic tree
using standard Python techniques, and to define specific __init__()
for your data.
- listener: a function, bound method or callable object. The first
argument will be a reference to a Message object.
The order of call of the listeners is not specified. Here are
examples of valid listeners (see the Sports.subscribe() calls)::
class Foo:
def __call__(self, m): pass
def meth(self, m): pass
def meth2(self, m, arg1=''): pass # arg1 is optional so valid
foo = Foo()
def func(m, arg1=None, arg2=''): pass # both arg args are optional
from yourmsgs import Sports
Sports.Hockey.subscribe(func) # function
Sports.Baseball.subscribe(foo.meth) # bound method
Sports.Hockey.subscribe(foo.meth2) # bound method
Sports.Hockey.subscribe(foo) # functor (Foo.__call__)
In every case, the parameter `m` will contain the message instance,
and the remaining arguments are those given to the message constructor.
- message: an instance of a message of a certain type. You create the
instance, giving it data via keyword arguments, which become instance
attributes. E.g. ::
from yourmsgs import sendMessage, Sports
sendMessage( Sports.Hockey(a=1, b='c') )
will cause the previous example's `func` listener to get an instance
m of Sports.Hockey, with m.a==1 and m.b=='c'.
Note that every message instance has a subTopic attribute. If this
attribute is not None, it means that the message instance is
not for the topic given to the sendMessage(), but for a more
generic topic (closer to the root of the message type tree)::
def handleSports(msg):
assert msg.subTopic == Sports.Hockey
def handleHockey(msg):
assert msg.subTopic == None
Sports.Hockey.subscribe(handleHockey)
Sports.subscribe(handleSports)
sendMessage(Sports.Hockey())
- sender: the part of your code that calls send()::
# Sports.Hockey is defined in yourmsgs.py, so:
from yourmsgs import sendMessage, Sports
# now send something:
msg = Sports.Hockey(arg1)
sendMessage( msg )
Note that the above will cause your listeners to be called as
f(msg, arg1).
- log output: using a messaging system has the disadvantage that
"tracking" data/events can be more difficult. As an aid,
information is sent to a log function, which by default just
discards the information. You can set your own logger via
setLog() or logToStdOut().
An extra string can be given in the send() or
subscribe() calls. For send(), this string allows you to identify
the "send point": if you don't see it on your log output, then
you know that your code doesn't reach the call to send(). For
subscribe(), it identifies the listener with a string of your choice,
otherwise it would be the (rather cryptic) Python name for the listener
callable.
- exceptions while sending: what should happen if a listener (or something
it calls) raises an exception? The listeners must be independent of each
other because the order of calls is not specified. Certain types of
exceptions might be handlable by the sender, so simply stopping the
send loop is rather extreme. Instead, the send() aggregates the exception
objects and when it has sent to all listeners, raises a ListenerError
exception. This has an attribute `exceptions` that is a list of
ExcInfo instances, one for each exception raised during the send().
- infinite recursion: it is possible, though not likely, that one of your
messages causes another message to get sent, which in turn causes the
first type of message to get sent again, thereby leading to an infinite
loop. There is currently no guard against this, though adding one would
not be difficult.
To summarize:
- First, create a file e.g. yourmsgs.py in which you define and document
your message topics tree and in which you call setupMsgTree();
- Subscribe your listeners to some of those topics by importing yourmsgs.py,
and calling subscribe() on the message topic to listen for;
- Anywhere in your code, you can send a message by importing yourmsgs.py,
and calling `sendMessage( MsgTopicSeq(data) )` or MsgTopic(data).send()
- Debugging your messaging:
- If you are not seeing all the messages that you expect, add some
identifiers to the send/subscribe calls.
- Turn logging on with logToStdOut() (or use setLog(yourLogFunction)
- The class mechanism will lead to runtime exception if msg topic doesn't
exist.
Note: Listeners (callbacks) are held only by weak reference, which in
general is adequate (this prevents the messaging system from keeping alive
callables that are no longer used by anyone). However, if you want the
callback to be a wrapper around one of your functions, that wrapper must
be stored somewhere so that the weak reference isn't the only reference
to it (which will cause it to die).
:Author: Oliver Schoenborn
:Since: Apr 2004
:Version: 2.01
:Copyright: \(c) 2007-2009 Oliver Schoenborn
:License: see LICENSE.txt
'''
PUBSUB_VERSION = 2
VERSION_STR = "2.0a.200810.r153"
import sys, traceback
from core import weakmethod
__all__ = [
# listener stuff:
'Listener', 'ListenerError', 'ExcInfo',
# topic stuff:
'Message',
# publisher stuff:
'subscribe', 'unsubscribe', 'sendMessage',
# misc:
'PUBSUB_VERSION', 'logToStdOut', 'setLog', 'setupMsgTree',
]
def subscribe(listener, MsgClass, id=None):
'''DEPRECATED (use MsgClass.subscribe() instead). Subscribe
listener to messages of type MsgClass.
If id is given, it is used to identify the listener in a more
human-readable fashion in log messages. Note that log messages
are only produced if setLog() was given a non-null writer. '''
MsgClass.subscribe(listener, id)
def unsubscribe(listener, MsgClass, id=None):
'''DEPRECATED (use MsgClass.subscribe() instead). Unsubscribe
listener to messages of type MsgClass.
If id is given, it is used to identify the listener in a more
human-readable fashion in log messages. Note that log messages
are only produced if setLog() was given a non-null writer. '''
MsgClass.unsubscribe(listener, id)
def sendMessage(msg, id=None):
'''Send a message to its registered listeners. The msg is an instance of
class derived from Message. If id is given, it is used to identify the
sender in a more human-readable fashion in log messages. Note that log
messages are only produced if setLog() was given a non-null writer. Note
also that all listener exceptions are caught, so that all listeners get
a chance at receiving the message. Once all
listeners have been sent the message, a ListenerException will be raised
containing a list of all exceptions raised during the send.'''
msg.send(id)
class ExcInfo:
'''Represent an exception raised by a listener. It contains the info
returned by sys.exc_info() (self.type, self.arg, self.traceback), as
well as the sender ID (self.senderID), and ID of listener that raised
the exception (self.listenerID).'''
def __init__(self, senderID, listenerID, excInfo):
self.type = excInfo[0] # class of exception
self.arg = excInfo[1] # value given to constructor
self.traceback = excInfo[2] # traceback
self.senderID = senderID or 'anonymous' # id of sender for which raised
self.listenerID = listenerID # id of listener in which raised
def __str__(self):
'''Regular stack-trace message'''
return ''.join(traceback.format_exception(
self.type, self.arg, self.traceback))
class ListenerError(RuntimeError):
'''Gets raised when one or more listeners raise an exception
while they receive a message.
An attribute `exceptions` is a list of ExcInfo objects, one for each
exception raised.'''
def __init__(self, exceps):
self.exceptions = exceps
RuntimeError.__init__(self, '%s exceptions raised' % len(exceps))
def getTracebacks(self):
'''Get a list of strings, one for each exception's traceback'''
return [str(ei) for ei in self.exceptions]
def __str__(self):
'''Create one long string, where tracebacks are separated by ---'''
sep = '\n%s\n\n' % ('-'*15)
return sep.join( self.getTracebacks() )
# the logger used by all text output; defaults to null logger
_log = None
def setLog(writer):
'''Set the logger used by this module. The 'writer' must be a
callable taking one argument (a text string to be logged),
or an object that has a write() method, or None to turn off logging.
If this function is not called, no logging occurs. Setting a logger
may be useful to help discover when certain messages are sent but
not received, etc. '''
global _log
if callable(writer):
_log = writer
elif writer is not None:
_log = writer.write
else:
_log = None
def logToStdOut():
'''Shortcut for import sys; setLog(sys.stdout). '''
import sys
setLog(sys.stdout)
def setupMsgTree(RootClass, yourModuleLocals=None):
'''Call this function to setup your message module for use by pubsub2.
The RootClass is your class (derived from Message) that is at the root
of your message tree. The yourModuleLocals, if given, should be
locals(). E.g.
#yourMsgs.py:
import pubsub2 as ps
class A(ps.Message):
class B(ps.Message):
pass
ps.setupMsgTree(A, locals())
The above does two things: 1. when a message of type B eventually
gets sent, listeners for messages of type A will also receive it
since A is more generic than B; 2. when a module does
"import yourMsgs", that module sees pubsub2's functions and
classes as though they were in yourMsgs.py, so you can write
e.g. "yourMsgs.sendMessage()" rather than "yourMsgs.pubsub2.sendMessage()"
or "import pubsub2; pubsub2.sendMessage()". '''
RootClass._setupChaining()
if yourModuleLocals is not None:
gg = [(key, val) for key, val in globals().iteritems()
if not key.startswith('_') and key not in ('setupMsgTree','weakmethod')]
yourModuleLocals.update(dict(gg))
class Listener:
'''
Represent a listener of messages of a given class. An identifier
string can accompany the callback, it will be used in text messages.
Note that callback must give callable(callback) == True.
Note also that two Listener object compare as equal if they
are for the same callback, regardless of id:
>>> Listener(cb, 'id1') == Listener(cb, 'id2')
True
'''
def __init__(self, callback, id=None):
assert callable(callback), '%s is not callable' % callback
self.__callable = weakmethod.getWeakRef(callback)
self.id = id
self.weakID = str(self) # save this now in case callable weak ref dies
def getCallable(self):
'''Get the callback that was given at construction. Note that
this could be None if it no longer exists in system (if it was
created as a wrapper of some other callable, and not stored
locally).'''
return self.__callable()
def __call__(self, *args, **kwargs):
cb = self.__callable()
if cb:
cb(*args, **kwargs)
else:
msg = 'Callback %s no longer exists (maybe it was wrapped?)' % self.weakID
raise RuntimeError(msg)
def __eq__(self, rhs):
return self.__callable() == rhs.__callable()
def __str__(self):
'''String rep is the id, if given, or if not, the str(callback)'''
return self.id or str(self.__callable())
class Message:
'''
Represent a message to be sent from a sender to a listener.
This class should be derived, and the derived class should
be documented, to help explain the message and its data.
E.g. provide a documented __init__() to help explain the data
carried by the message, the purpose of this type of message, etc.
'''
_listeners = None # class-wide registry of listeners
_parentClass = None # class-wide parent of messages of our type
_type = 'Message' # a string for type
_childrenClasses = None # keep track of children
def __init__(self, subTopic=None, **kwargs):
'''The kwargs will be given to listener callback when
message delivered. Subclasses of Message can define an __init__
that has specific attributes to better document the message
data.'''
self.__kwargs = kwargs
self.subTopic = subTopic
def __getattr__(self, name):
if name not in self.__kwargs:
raise AttributeError("%s instance has no attribute '%s'" \
% (self.__class__.__name__, name))
return self.__kwargs[name]
def send(self, senderID=None):
'''Send this instance to registered listeners, including listeners
of more general versions of this message topic. If any listener raises
an exception, a ListenerError is raised after all listeners have been
sent the message. The senderID is used in logged output (if setLog()
was called) and in ListenerError. '''
exceps = self.__deliver(senderID)
# make parents up chain send with same data
ParentCls = self._parentClass
while ParentCls is not None:
subTopic = self.subTopic or self.__class__
msg = ParentCls(subTopic=subTopic, **self.__kwargs)
ParentCls, exceptInfo = msg.sendSpecific(senderID)
exceps.extend(exceptInfo)
if exceps:
raise ListenerError(exceps)
def sendSpecific(self, senderID=None):
'''Send self to registered listeners, but don't "continue up the
message tree", ie listeners of more general versions of this topic
will not receive the message. See send() for description of senderID.
Returns self's parent message class and a list of exceptions
raised by listeners.'''
exceptInfo = self.__deliver(senderID)
return self._parentClass, exceptInfo
def __deliver(self, senderID):
'''Do the actual message delivery. Logs output if setLog() was
called, and accumulates exception information.'''
if not self._listeners:
if _log and senderID:
_log( 'No listeners of %s for sender "%s"\n'
% (self.getType(), senderID) )
return []
if _log and senderID:
_log( 'Message of type %s from sender "%s" should reach %s listeners\n'
% (self.getType(), senderID, len(self._listeners)) )
received = 0
exceptInfo = []
for listener in self._listeners:
if _log and (senderID or listener.id):
_log( 'Sending message from sender "%s" to listener "%s"\n'
% (senderID or 'anonymous', str(listener)))
try:
listener(self)
received += 1
except Exception:
excInfo = ExcInfo(senderID, str(listener), sys.exc_info())
exceptInfo.append( excInfo )
if _log and senderID:
_log( 'Delivered message from sender "%s" to %s listeners\n'
% (senderID, received))
return exceptInfo
@classmethod
def getType(cls):
'''Return a string representing the type of this message,
e.g. A.B.C.'''
return cls._type
@classmethod
def hasListeners(cls):
'''Return True only if at least one listener is registered
for this class of messages.'''
return cls._listeners is not None
@classmethod
def hasListenersAny(cls):
'''Return True only if at least one listener is registered
for this class of messages OR any of the more general topics.'''
hasListeners = cls.hasListeners()
parent = cls._parentClass
while parent and not hasListeners:
hasListeners = parent.hasListeners()
parent = parent._parentClass
return hasListeners
@classmethod
def countListeners(cls):
'''Count how many listeners this class has registered'''
if cls._listeners:
return len(cls._listeners)
return 0
@classmethod
def countAllListeners(cls):
'''Count how many listeners will get this type of message'''
count = cls.countListeners()
parent = cls._parentClass
while parent:
count += parent.countListeners()
parent = parent._parentClass
return count
@classmethod
def subscribe(cls, who, id=None):
'''Subscribe `who` to messages of our class.'''
if _log and id:
_log( 'Subscribing %s to messages of type %s\n'
% (id or who, cls.getType()) )
listener = Listener(who, id)
if cls._listeners is None:
cls._listeners = [listener]
else:
if listener in cls._listeners:
idx = cls._listeners.index(listener)
origListener = cls._listeners[idx]
if listener.id != origListener.id:
if _log:
_log('Changing id of Listener "%s" to "%s"\n'
% (origListener.id or who, listener.id or 'anonymous'))
origListener.id = listener.id
elif _log and listener.id:
_log( 'Listener %s already subscribed (as "%s")\n' % (who, id) )
else:
cls._listeners.append( listener )
@classmethod
def unsubscribe(cls, listener):
'''Unsubscribe the given listener (given as `who` in subscribe()).
Does nothing if listener not registered. Unsubscribes all direct
listeners if listener is the string 'all'. '''
if listener == 'all':
cls._listeners = None
if _log:
_log('Unsubscribed all listeners')
return
ll = Listener(listener)
try:
idx = cls._listeners.index(ll)
llID = cls._listeners[idx].id
del cls._listeners[idx]
except ValueError:
if _log:
_log('Could not unsubscribe listener "%s" from %s' \
% (llID or listener, cls._type))
else:
if _log:
_log('Unsubscribed listener "%s"' % llID or listener)
@classmethod
def clearSubscriptions(cls):
'''Unsubscribe all listeners of this message type. Same as
unsubscribe('all').'''
cls.unsubscribe('all')
'''Remove all registered listeners from this type of message'''
cls._listeners = None
@classmethod
def getListeners(cls):
'''Get a list of listeners for this message class. Each
item is an instance of Listener.'''
#_log( 'Listeners of %s: %s' % (cls, cls._listeners) )
if not cls._listeners:
return []
return cls._listeners[:] # return a copy!
@classmethod
def getAllListeners(cls):
'''This returns all listeners that will be notified when a send()
is done on this message type. The return is a dictionary where
key is message type, and value is the list of listeners registered
for that message type. E.g. A.B.getAllListeners() returns
`{['A':[lis1,lis2],'A.B':[lis3]}`.'''
ll = {}
ll[cls._type] = cls.getListeners()
parent = cls._parentClass
while parent:
parentLL = parent.getListeners()
if parentLL:
ll[parent._type] = parentLL
parent = parent._parentClass
return ll
@classmethod
def _setupChaining(cls, parents=None):
'''Chain all the message classes children of cls so that, when a
message of type 'cls.childA.subChildB' is sent, listeners of
type cls.childA and of type cls get it too. '''
# parent:
if parents:
cls._parentClass = parents[-1]
lineage = parents[:] + [cls]
cls._type = '.'.join(item.__name__ for item in lineage)
if _log:
_log( '%s will chain up to %s\n'
% (cls._type, cls._parentClass.getType()) )
else:
cls._parentClass = None
lineage = [cls]
cls._type = cls.__name__
if _log:
_log( '%s is at root (top) of messaging tree\n' % cls._type )
# go down into children:
cls._childrenClasses = []
for childName, child in vars(cls).iteritems():
if (not childName.startswith('_')) and issubclass(child, Message):
cls._childrenClasses.append(child)
child._setupChaining(lineage)
#---------------------------------------------------------------------------
import pubsubconf
pubsubconf.pubModuleLoaded()
del pubsubconf
|