diff --git a/g13gui/g13gui/observer.py b/g13gui/g13gui/observer.py new file mode 100644 index 0000000..7360cb1 --- /dev/null +++ b/g13gui/g13gui/observer.py @@ -0,0 +1,84 @@ +#!/usr/bin/python + +from enum import Enum + + +class ChangeType(Enum): + ADD = 0 + REMOVE = 1 + MODIFY = 2 + + +class Observer(object): + """Simple interface class to handle Observer-style notifications""" + + def onSubjectChanged(self, subject, changeType, key, changeData): + """Event handler for observer notifications. + + Each subclass of Observer MUST override this method. There is no default + method for handling events of this nature. + + subject[object]: the subject object that sent the event notifying something + changed in its data model. + changeType[ChangeType]: the type of change that occurred. + key[string]: a required name for what field changed. + whatChanged[object]: an optional context-dependent object, dict, or + None, specifying what changed. In the case of an ADD or MODIFY, + whatChanged should be the new data. In the case of a DELETE, it should + be the old data (or None). + """ + raise NotImplementedError( + "%s did not override Observer#onSubjectChanged!" % (type(self))) + + +class Subject(object): + """Simple class to handle the subject-side of the Observer pattern.""" + + def registerObserver(self, observer): + """Registers an Observer class as an observer of this object""" + if '_observers' not in self.__dict__: + self._observers = {observer} + else: + self._observers.add(observer) + + def removeObserver(self, observer): + """Removes an observer from this object""" + if '_observers' in self.__dict__: + if observer in self._observers: + self._observers.discard(observer) + + def addChange(self, type, key, whatChanged=None): + """Schedules a change notification for transmitting later. + + type[ChangeType]: the type of change that occurred. + key[string]: a required name for what field changed. + whatChanged[object]: an optional context-dependent object, dict, or + None, specifying what changed. In the case of an ADD or MODIFY, + whatChanged should be the new data. In the case of a DELETE, it should + be the old data (or None). + """ + if '_changes' not in self.__dict__: + self._changes = [(type, key, whatChanged)] + else: + self._changes.append((type, key, whatChanged)) + + def clearChanges(self): + """Removes all scheduled changes from the change buffer.""" + self._changes = [] + + def notifyChanged(self): + """Notifies all observers of scheduled changes in the change buffer. + + This method actually does the work of iterating through all observers + and all changes and delivering them to the Observer's onSubjectChanged + method. + + It is safe to call this if there are no changes to send in the buffer, + or there are no observers to send changes to. Note that calling this + when no observers are registered will still flush the change buffer. + """ + if '_observers' in self.__dict__ and '_changes' in self.__dict__: + for observer in self._observers: + for change in self._changes: + observer.onSubjectChanged(self, *change) + self._changes = [] diff --git a/g13gui/g13gui/observer_tests.py b/g13gui/g13gui/observer_tests.py new file mode 100644 index 0000000..5ea57d7 --- /dev/null +++ b/g13gui/g13gui/observer_tests.py @@ -0,0 +1,76 @@ +#!/usr/bin/python + +import unittest +import observer + +class TestObserver(observer.Observer): + def __init__(self): + self.changes = [] + + def onSubjectChanged(self, subject, type, key, whatChanged): + self.changes.insert(0, { + 'subject': subject, + 'type': type, + 'key': key, + 'whatChanged': whatChanged + }) + + def assertChangeNotified(self, subject, type, key): + change = self.changes.pop() + assert(change['subject'] == subject) + assert(change['type'] == type) + assert(change['key'] == key) + return change['whatChanged'] + + +class TestIncorrectObserver(observer.Observer): + pass + + +class TestSubject(observer.Subject): + pass + + +class ObserverTestCase(unittest.TestCase): + def setUp(self): + self.subject = TestSubject() + + def testRegistration(self): + observer = TestObserver() + self.subject.registerObserver(observer) + assert(len(self.subject._observers) == 1) + + self.subject.registerObserver(observer) + assert(len(self.subject._observers) == 1) + + self.subject.removeObserver(observer) + assert(len(self.subject._observers) == 0) + + self.subject.removeObserver(observer) + + def testSubclassNotificationError(self): + testObserver = TestIncorrectObserver() + self.subject.addChange(observer.ChangeType.ADD, 'foo') + self.subject.registerObserver(testObserver) + + try: + self.subject.notifyChanged() + except NotImplementedError: + pass + else: + unittest.fail('Expected NotImplementedError') + + def testSubclassNotification(self): + o = TestObserver() + self.subject.registerObserver(o) + + self.subject.addChange(observer.ChangeType.ADD, 'foo', 'bar') + self.subject.notifyChanged() + + result = o.assertChangeNotified( + self.subject, observer.ChangeType.ADD, 'foo') + assert(result == 'bar') + + +if __name__ == '__main__': + unittest.main()