The IsChangedMixin can be added to any class and queried to determine if the class instance contents, or any other instances contained in the class instance, have been altered.
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 | import types, copy
SeqType = [types.DictType, types.ListType]
class IsChangedMixin:
def __init__(self):
self.ResetChanges()
def ResetChanges(self):
""" Create a snapshot of own namespace dictionary. Halfway between a
shallow and a deep copy - recursively make shallow copies of all
lists and dictionaries but object instances are copied as-is. Such
objects will have their own IsModified attribute if they need to be
tested for modification
"""
self._snapshot = self._CopyItem(self.__dict__)
def _CopyItem(self, item):
""" Return shallow copy of item. If item is a sequence, recursively
shallow copy each member that is also a sequence
"""
newitem = copy.copy(item)
if type(newitem) is types.DictType:
for key in newitem:
if type(newitem[key]) in SeqType:
newitem[key] = self._CopyItem(newitem[key])
elif type(newitem) is types.ListType:
for k in range(len(newitem)):
if type(newitem[k]) in SeqType:
newitem[k] = self._CopyItem(newitem[k])
return newitem
def IsModified(self):
""" Return True if current namespace dictionary is different to snapshot
Examine contained objects having an IsModified attribute
and return True if any of them are True.
"""
return self._CheckSequence(self.__dict__, self._snapshot, checklen=False)
def _CheckSequence(self, newseq, oldseq, checklen=True):
""" Scan sequence comparing new and old values of individual items
return True when the first difference is found.
Compare sequence lengths if checklen is True. It is False on first
call because self.__dict__ has _snapshot as an extra entry
"""
if checklen and len(newseq) <> len(oldseq):
return True
if type(newseq) is types.DictType:
for key in newseq:
if key == '_snapshot':
continue
if key not in oldseq:
return True
if self._CheckItem(newseq[key], oldseq[key]):
return True
else:
for k in range(len(newseq)):
if self._CheckItem(newseq[k], oldseq[k]):
return True
return 0
def _CheckItem(self, newitem, olditem):
""" Compare the values of newitem and olditem.
If item types are sequences, make recursive call to _CheckSequence.
If item types are instances of objects with an IsModified attribute,
return True if IsModified() is True,
otherwise return True if items are different
"""
if type(newitem) in SeqType:
return self._CheckSequence(newitem, olditem)
elif type(newitem) is types.InstanceType:
if newitem is not olditem: # not the same instance
return True
if hasattr(newitem, 'IsModified') and newitem.IsModified():
return True
elif newitem <> olditem:
return True
else:
return False
|
A frequent real-life requirement, eg. when a user closes a window, is to check for changes and warn that, if continued, all changes will be lost. If the user-changeable content is contained (if only temporarily) in a single class instance, the IsChangedMixin can be used for this purpose:
MyDataClass([... ,] IsChangedMixin):
It is not usually necessary to call the __init__ method of the mixin, but to take a snapshot after the initial data load eg:
...
self.ResetChanges()
In a typical application, the instance may contain many instances of other classes as in the (tired old) example of an invoice, containing header data and an unlimited number of detail lines, or instances. In this case the details class would also use an IsChangedMixin and each instance would take a snapshot of its initial state.
There is no reason why the detail-instances cannot themselves contain other instances, such as product-details. In practice those are not likely to be modifiable in the current activity, but may well be modifiable elsewhere. For this reason, it can be useful to add a default "use_mixin=False" parameter to the class constructor, to avoid the overhead of copying self.__dict__ except when necessary.
The original implementation of this recipe used copy.deepcopy() for each __dict__, but when the number of contained instances reached several hundred, the time overhead grew correspondingly to several seconds. Using a recursive shallow copy does not noticeably increase the total time to instantiate and populate the whole data structure.
When required, the check for modification is relatively simple, such as:
On Close(): __d = .... __if d.IsModified(): ____if not confirm('Not saved. Changes will be lost.'): ______return # abort the close __d.Close()
Also, when changes have been saved or committed, a fresh snapshot, i.e. self.Resetchanges(), should be taken at the end of the 'Save' method of each affected class.
If any class that uses the mixin contains state-data, that is, instance variables that may change but which do not form part of the user-modified data, these can be delegated to an instance of a purpose-declared, subordinate class (without a mixin), so that while remaining accessible to the main class, they are not visible to the IsModified() method.