Source

wxPython / wx / lib / pubsub / pubsub2 / pub.py

Full commit
  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