Welcome, guest | Sign In | My Account | Store | Cart
import weakref
import exceptions
import threading   # for lock

class RecursionError(exceptions.RuntimeError):
    pass

class _subscription(object):
    """A subscription is returned as a result of subscribing to an 
       observable. When the subscription object is finalized, the 
       subscription is cancelled.  This class is used to facilitate 
       subscription cancellation."""

    def __init__(self, subscriber, observed):
        self.subscriber = subscriber
        self.observed = weakref.ref(observed)

    def __del__(self):
        obsrvd = self.observed()
        if (self.subscriber and obsrvd):
            obsrvd._cancel(self.subscriber)


class _obwan(object):
    '''Half-hidden class.  Only 'observable should construct these.
    Calls to subscribe, cancel get invoked through the observable.
    _obwan objects reside in class instances containing observables.'''

    def __init__(self):
        self.subscribers = []
        self._value = None
        self._changeLock = threading.Lock()
        
    def __call__(self):
        """returns the current value, the one last set"""
        return self._value

    def _notifySubscribers(self, value):
        for (f,exceptionHdlr) in self._callbacks():
            try:
                f(value)
            except Exception, ex:
                if exceptionHdlr and not exceptionHdlr(ex): 
                    raise            # reraise if not handled

    def setvalu(self, value):
        """Notify the subcribers only when the value changes."""
        if self._value != value:
            if self._changeLock.acquire(0):     # non-blocking
                self._value = value
                try:
                    self._notifySubscribers(value)
                finally:
                    self._changeLock.release()
            else:
                raise RecursionError("Attempted recursion into observable's set method.")

    def subscribe(self, obsv, exceptionInfo = None):
        observer = obsv.setvalu if isinstance(obsv, _obwan) else obsv
        ob_info =(observer, exceptionInfo)
        self.subscribers.append(ob_info)
        return _subscription(ob_info, self)

    def _callbacks(self):
       scribers = []
       scribers.extend(self.subscribers)
       return scribers

    def _cancel(self, wref):
        self.subscribers.remove(wref)


class Observable(object):
    """An observable implemented as a descriptor. Subscribe to an observable 
    via calling  xxx.observable.subscribe(callback)"""
    def __init__(self, nam):
        self.xname = "__"+nam
        self.obwan = _obwan

    def __set__(self,inst, value ):
        """set gets the instances associated variable and calls 
        its setvalu method, which notifies subribers"""
        if inst and not hasattr(inst, self.xname):
            setattr(inst, self.xname, self.obwan())
        ow = getattr(inst, self.xname)
        ow.setvalu(value)

    def __get__(self, inst, klass):
        """get gets the instances associated variable returns it"""
        if inst and not hasattr(inst, self.xname):
            setattr(inst, self.xname, self.obwan())
        return getattr(inst, self.xname)

#-----------------------------------------------------------------------
#   Example & Simple Test
#-----------------------------------------------------------------------
if __name__ == '__main__':

    class MyClass(object):
        """ A Class containing the observables length and width"""
        length = Observable('length')
        width = Observable('width')

        def __init__(self):
            self.length.setvalu(0)
            self.width.setvalu(0)
            

    class MyObserver(object):
        """An observer class. The initializer is passed an instance
           of 'myClass' and subscribes to length and width changes.
           This observer also itself contains an observable, l2"""
        
        l2 = Observable('l2')

        def __init__(self, name, observedObj):
            self.name = name
            self.subs1 = observedObj.length.subscribe(self.print_l)
            self.subs2 = observedObj.width.subscribe(self.print_w)
            
            """An observable can subscribe to an observable, in which case
              a change will chain through both subscription lists.
              Here l2's subscribers will be notified whenever observedObj.length
              changes"""
            self.subs3 = observedObj.length.subscribe(self.l2)

        def print_w(self, value):
            print "%s Observed Width"%self.name, value

        def print_l(self, value):
            print "%s Observed Length"%self.name, value
            
        def cancel(self):
            """Cancels the instances current subscriptions. Setting self.subs1 to
            None removes the reference to the subscription object, causing it's 
            finalizer (__del__) method to be called."""
            self.subs1 = None
            self.subs2 = None
            self.subs3 = None

    def pl2(value):
        print "PL2 reports ", value
        if type(value) == type(3):
            raise ValueError("pl2 doesn't want ints.")

    def handlePl2Exceptions( ex ):
        print 'Handling pl2 exception:', ex, type(ex)
        return True     # true if handled, false if not
            
    area = MyClass()
    kent = MyObserver("Kent", area)
    billy = MyObserver("Billy", area)
    subscription = billy.l2.subscribe(pl2, handlePl2Exceptions)

    area.length = 6
    area.width = 4
    area.length = "Reddish"

    billy.subs1 = None
    print "Billy shouldn't report a length change to 5.15."
    area.length = 5.15      
    billy.cancel()
    print "Billy should no longer report"
    area.length = 7
    area.width = 3
    print "Areas values are ", area.length(), area.width()

    print "Deleting an object with observables having subscribers is ok"
    area = None
    area = MyClass()
    print "replaced area - no subscribers to this new instance"
    area.length = 5
    area.width ="Bluish"
    c = raw_input("ok? ")

History

  • revision 5 (14 years ago)
  • previous revisions are not available