class ListenableSet(set):
'''A set that notifies listeners whenever it is updated
Callback order and signature:
for cb in self.listeners: cb(self)
'''
def __init__(self, *args, **kwds):
self._listeners = []
set.__init__(self, *args, **kwds)
def notify(self):
'Run all the callbacks in self.listeners.'
for cb in self._listeners:
cb(self)
@property
def listeners(self):
'Read-only access to _listeners'
return self._listeners
def _detect_size_change(methodname, parent):
method = getattr(parent, methodname)
def wrapper(self, *args, **kwds):
original_size = len(self)
result = method(self, *args, **kwds)
if len(self) != original_size:
for cb in self._listeners:
cb(self)
return result
wrapper.__name__ = methodname
wrapper.__doc__ = method.__doc__
return wrapper
def _detect_any_call(methodname, parent):
method = getattr(parent, methodname)
def wrapper(self, *args, **kwds):
result = method(self, *args, **kwds)
for cb in self._listeners:
cb(self)
return result
wrapper.__name__ = methodname
wrapper.__doc__ = method.__doc__
return wrapper
for methodname in 'clear add pop discard remove update difference_update ' \
'__isub__ __iand__ __ior__ intersection_update'.split():
locals()[methodname] = _detect_size_change(methodname, set)
for methodname in '__ixor__ symmetric_difference_update'.split():
locals()[methodname] = _detect_any_call(methodname, set)
# ---- Example callback to print information on size -------------------------
def notice(s):
print 'Set at %d now has size %d\n' % (id(s), len(s))
s = ListenableSet('abcdefgh')
s.listeners.append(notice)
s.add('i') # adding a new element changes the set and triggers a callback
s.add('a') # adding an existing element results in no change or callback
s.listeners.remove(notice)
s.add('j') # without a listener, works just like a regular set
# ---- Example callback to report changes in the set -------------------------
def change_report(s, prev=set()):
added = s - prev
if added:
print 'Added elements: ', list(added)
removed = prev - s
if removed:
print 'Removed elements: ', list(removed)
if added or removed:
print
prev.clear()
prev.update(s)
s = ListenableSet()
s.listeners.append(change_report)
s.update('abracadabra')
s ^= set('simsalabim')
# ---- Example callback to enforce a set invariant ---------------------------
def enforce_lowercase(s):
for elem in s:
if not elem.islower():
raise ValueError('Set member must be lowercase: ' + repr(elem))
s = ListenableSet()
s.listeners.append(enforce_lowercase)
s.add('slartibartfast')
s.add('Bang') # Raises an exception because the set element isn't lowercase