Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 165 lines
  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

Created by Oran Looney on Sun, 3 Jun 2007 (PSF)
Python recipes (4591)
Oran Looney's recipes (1)

Required Modules

Other Information and Tasks