g13gui: Add in an observer pattern

We'll need this as complexity of the GUI grows. This allows us to queue up
change notifications and pass them to observing objects more cleanly.
This commit is contained in:
June Tate-Gans 2021-04-27 13:45:57 -05:00
parent bac31a772a
commit eb816c8acb
2 changed files with 160 additions and 0 deletions

84
g13gui/g13gui/observer.py Normal file
View File

@ -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 = []

View File

@ -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()