First, a Dictionary class that 'journals' sets and dels. The changes in the journal can either be applied (like committing a transaction) or wiped (like rollback.)
The journalled dictionary is used to implement a JournalledMixin class that gives journalling/transaction behavor to any object.
Links and discussion below.
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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | '''
JournalledDict
Implements the dictionary interface (with the help of DictMixin).
In this implementation, changes are not immediately stored, but instead
tracked in a journal. When the value of a key is requested,
the journal is checked first and provides the most recent value.
The method apply() replays the journal to update the
underlying dictionary, and wipe() throws away all
recent changes without saving. Both clear the journal.
I designed this class to implement transaction-like behavor
on local objects; you can add that behavor to a class with the
JournalledMixin.
JournalledDict only implements the minimal set of special
methods needed for DictMixin to provide the full interface.
So, methods like iteritems() will not give good performance.
Note that mutating the values of attributes is not (and cannot)
be journalled.
'''
import UserDict
class Change(object):
' A single set or del. The journal is a list of Changes. '
def __init__(self, action, key, value):
self.action = action
self.key = key
self.value = value
def __repr__(self):
return '<%s%s:%s>' % (self.action, self.key,self.value)
class JournalledDict(UserDict.DictMixin):
'A dictionary with journalling behavor.'
def __init__(self,prototype=None,**kwargs):
self.base = prototype if prototype else {}
self.base.update(kwargs)
self.changes = []
def __setitem__(self,key,value):
# add to the front of the journal
self.changes[0:0] = [ Change('+',key,value) ]
def __delitem__(self,key):
self[key] # correctly raises KeyError if the key isn't defined
self.changes[0:0] = [ Change('-',key,None) ]
def __getitem__(self, key):
for change in self.changes:
if change.key == key:
if change.action == '+':
return change.value
elif change.action == '-':
raise KeyError,key
return self.base[key]
def apply(self):
'play the journal back onto the base dictionary.'
for change in self.changes:
if change.action == '+':
self.base[change.key] = change.value
elif change.action == '-':
del self.base[change.key]
self.wipe()
def wipe(self):
'clear the journal, reverting to the last applied state.'
self.changes = []
def keys(self):
'provide an iterable of keys.'
keys = set(self.base.keys())
for change in self.changes:
if change.action == '+':
keys.add(change.key)
elif change.action == '-':
keys.remove(change.key)
return keys
class JournalledMixin(object):
'Gives an class journaling behavor.'
'JournalledMixin should be listed FIRST in the list of base classes.'
def __init__(self, *args, **kwargs):
self.__dict__['_journal'] = JournalledDict(self.__dict__)
super(JournalledMixin, self).__init__(*args, **kwargs)
# Save the state after giving other bases classes a chance to initialize.
JournalledMixin.applyJournal(self)
def __getattr__(self, name): return self.__dict__['_journal'][name]
def __setattr__(self, name, value): self.__dict__['_journal'][name] = value
def __delattr__(self, name): del self.__dict__['_journal'][name]
def wipeJournal(self):
'clear the journal and throw away all recent changes.'
self.__dict__['_journal'].wipe()
def applyJournal(self):
'Save all recent changes and start a new journal.'
self.__dict__['_journal'].apply()
### Test Cases ###
if __name__ == '__main__':
class Position(object):
'A trivial class used to test the Mixin.'
def __init__(self,x=0,y=0):
self.x = x
self.y = y
def __str__(self):
return '(%s, %s)' % (self.x, self.y)
class JournalledPosition(JournalledMixin, Position):
'A trivial class with journalling behavor.'
pass
print 'Basic Example:'
print '--------------'
print 'start with a small Journalled Dictionary:'
jd = JournalledDict(x=1,y=2)
print jd.base
print jd.changes
jd['x'] = 4
jd['z'] = 7
del jd['y']
print 'making a couple changes:'
print jd.base
print jd.changes
print 'now we apply the journal:'
jd.apply()
print jd.base
print jd.changes
print 'make a couple more changes:'
del jd['x']
jd['y'] = 15
print jd.base
print jd.changes
print 'and wipe the journal:'
jd.wipe()
print jd.base
print jd.changes
print
print
print 'Mixin Example:'
print '--------------'
jp = JournalledPosition(1,2)
print 'a fresh JournalledPosition instance:'
print jp
print 'we update the attributes:'
jp.x = 7
jp.y = -4
print jp
print 'but it goes right back after we call wipeJournal():'
jp.wipeJournal()
print jp
print 'we change x and call applyJournal():'
jp.x = 11
jp.applyJournal()
print jp
print 'now calling wipeJournal() does nothing:'
jp.wipeJournal()
print jp
|
There are several other recipes on the general subject of 'local transactions:' http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/413838 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/284677 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/306866
All are intended to revert or rollback changes in a simple way. This recipe takes the journal approach: each change to the dictionary (or object's __dict__) is intercepted and stored as an action/value pair in a list, while the underlying dictionary remains the same. However, when you access a key/attribute, you see the objects current state because changes in the journal are considered at each lookup.
So, this recipe is suitable for large dictionaries with immutable values, where a limited number of changes will be made between each commit/apply. (Performance degrades as the journal grows.)
None of these recipes will probably suit your problem very well; to REALLY make transaction behavor work, you'd have to journal every change to the state of the program, which is something that would have to be implemented in the Python engine itself. But I hope one of them will be reasonably close. :)
There are at least two Design Patterns related to Undo/rollback that you might be interested in: http://en.wikipedia.org/wiki/Command_pattern http://en.wikipedia.org/wiki/Memento_pattern