summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/attributes.py
blob: 9bdfcab6a1ec43358ccb672629c390c24a42dcda (plain)
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# attributes.py - manages object attributes
# Copyright (C) 2005 Michael Bayer mike_mp@zzzcomputing.com
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


import sqlalchemy.util as util
import weakref

class SmartProperty(object):
    """attaches AttributeManager functionality to the property accessors of a class.  all
    instances of the class will retrieve and modify their properties via an
    AttributeManager."""
    def __init__(self, manager):
        self.manager = manager
    def attribute_registry(self):
        return self.manager
    def property(self, key, uselist):
        def set_prop(obj, value):
            self.attribute_registry().set_attribute(obj, key, value)
        def del_prop(obj):
            self.attribute_registry().delete_attribute(obj, key)
        def get_prop(obj):
            if uselist:
                return self.attribute_registry().get_list_attribute(obj, key)
            else:
                return self.attribute_registry().get_attribute(obj, key)
                
        return property(get_prop, set_prop, del_prop)

class PropHistory(object):
    """manages the value of a particular scalar attribute on a particular object instance."""
    # make our own NONE to distinguish from "None"
    NONE = object()
    def __init__(self, obj, key, backrefmanager=None, **kwargs):
        self.obj = obj
        self.key = key
        self.orig = PropHistory.NONE
        self.backrefmanager = backrefmanager
    def gethistory(self, *args, **kwargs):
        return self
    def history_contains(self, obj):
        return self.orig is obj or self.obj.__dict__[self.key] is obj
    def setattr_clean(self, value):
        self.obj.__dict__[self.key] = value
    def getattr(self):
        return self.obj.__dict__[self.key]
    def setattr(self, value):
        if isinstance(value, list):
            raise ("assigning a list to scalar property '%s' on '%s' instance %d" % (self.key, self.obj.__class__.__name__, id(self.obj)))
        self.orig = self.obj.__dict__.get(self.key, None)
        self.obj.__dict__[self.key] = value
        if self.backrefmanager is not None and self.orig is not value:
            self.backrefmanager.set(self.obj, value, self.orig)
    def delattr(self):
        self.orig = self.obj.__dict__.get(self.key, None)
        self.obj.__dict__[self.key] = None
        if self.backrefmanager is not None:
            self.backrefmanager.set(self.obj, None, self.orig)
    def rollback(self):
        if self.orig is not PropHistory.NONE:
            self.obj.__dict__[self.key] = self.orig
            self.orig = PropHistory.NONE
    def commit(self):
        self.orig = PropHistory.NONE
    def added_items(self):
        if self.orig is not PropHistory.NONE:
            return [self.obj.__dict__[self.key]]
        else:
            return []
    def deleted_items(self):
        if self.orig is not PropHistory.NONE and self.orig is not None:
            return [self.orig]
        else:
            return []
    def unchanged_items(self):
        if self.orig is PropHistory.NONE:
            return [self.obj.__dict__[self.key]]
        else:
            return []

class ListElement(util.HistoryArraySet):
    """manages the value of a particular list-based attribute on a particular object instance."""
    def __init__(self, obj, key, data=None, backrefmanager=None, **kwargs):
        self.obj = obj
        self.key = key
        self.backrefmanager = backrefmanager
        # if we are given a list, try to behave nicely with an existing
        # list that might be set on the object already
        try:
            list_ = obj.__dict__[key]
            if data is not None:
                for d in data:
                    list_.append(d)
        except KeyError:
            if data is not None:
                list_ = data
            else:
                list_ = []
            obj.__dict__[key] = []
            
        util.HistoryArraySet.__init__(self, list_, readonly=kwargs.get('readonly', False))

    def gethistory(self, *args, **kwargs):
        return self
    def list_value_changed(self, obj, key, item, listval, isdelete):
        pass    
    def setattr(self, value):
        self.obj.__dict__[self.key] = value
        self.set_data(value)
    def delattr(self, value):
        pass    
    def _setrecord(self, item):
        res = util.HistoryArraySet._setrecord(self, item)
        if res:
            self.list_value_changed(self.obj, self.key, item, self, False)
            if self.backrefmanager is not None:
                self.backrefmanager.append(self.obj, item)
        return res
    def _delrecord(self, item):
        res = util.HistoryArraySet._delrecord(self, item)
        if res:
            self.list_value_changed(self.obj, self.key, item, self, True)
            if self.backrefmanager is not None:
                self.backrefmanager.delete(self.obj, item)
        return res

class CallableProp(object):
    """allows the attaching of a callable item, representing the future value
    of a particular attribute on a particular object instance, to 
    the AttributeManager.  When the attributemanager
    accesses the object attribute, either to get its history or its real value, the __call__ method
    is invoked which runs the underlying callable_ and sets the new value to the object attribute
    via the manager, at which point the CallableProp itself is dereferenced."""
    def __init__(self, manager, callable_, obj, key, uselist = False, live = False, **kwargs):
        self.manager = manager
        self.callable_ = callable_
        self.obj = obj
        self.key = key
        self.uselist = uselist
        self.live = live
        self.kwargs = kwargs

    def gethistory(self, passive=False, *args, **kwargs):
        if not self.uselist:
            if self.obj.__dict__.get(self.key, None) is None:
                if passive:
                    value = None
                else:
                    value = self.callable_()
                self.obj.__dict__[self.key] = value

            p = PropHistory(self.obj, self.key, **self.kwargs)
        else:
            if self.live or not self.obj.__dict__.has_key(self.key) or len(self.obj.__dict__[self.key]) == 0:
                if passive:
                    value =  None
                else:
                    value = self.callable_()
            else:
                value = None
            p = self.manager.create_list(self.obj, self.key, value, readonly=self.live, **self.kwargs)
        
        if not self.live:
            # set the new history list as the new attribute, discards ourself
            self.manager.attribute_history(self.obj)[self.key] = p
            self.manager = None
            # unless we are "live", in which case we stay around to execute again
        return p

    def commit(self):
        pass
    def rollback(self):
        pass

class BackrefManager(object):
    def __init__(self, key):
        self.key = key
    def append(self, parent, child):
        pass
    def delete(self, parent, child):
        pass
    def set(self, parent, child, oldchild):
        pass


class ListBackrefManager(BackrefManager):
    def append(self, parent, child):
        getattr(child, self.key).append(parent)
    def delete(self, parent, child):
        getattr(child, self.key).remove(parent)

class OneToManyBackrefManager(BackrefManager):
    def append(self, parent, child):
        setattr(child, self.key, parent)
    def delete(self, parent, child):
        setattr(child, self.key, None)

class ManyToOneBackrefManager(BackrefManager):
    def set(self, parent, child, oldchild):
        if oldchild is not None:
            try:
                getattr(oldchild, self.key).remove(parent)
            except:
                print "wha? oldchild is ", repr(oldchild)
        if child is not None:
            getattr(child, self.key).append(parent)
            
class AttributeManager(object):
    """maintains a set of per-attribute callable/history manager objects for a set of objects."""
    def __init__(self):
        pass

    def value_changed(self, obj, key, value):
        """subclasses override this method to provide functionality upon an attribute change of value."""
        pass
        
    def create_prop(self, key, uselist, **kwargs):
        return SmartProperty(self).property(key, uselist)
    def create_list(self, obj, key, list_, **kwargs):
        return ListElement(obj, key, list_, **kwargs)
    def create_callable(self, obj, key, func, uselist, **kwargs):
        return CallableProp(self, func, obj, key, uselist, **kwargs)
        
    def get_attribute(self, obj, key, **kwargs):
        try:
            return self.get_history(obj, key, **kwargs).getattr()
        except KeyError:
            raise AttributeError(key)

    def get_list_attribute(self, obj, key, **kwargs):
        return self.get_history(obj, key, **kwargs)
        
    def set_attribute(self, obj, key, value, **kwargs):
        if key == 'parent' and value is not None and value.__class__.__name__ != 'Comment':
            raise "wha?"
        self.get_history(obj, key, **kwargs).setattr(value)
        self.value_changed(obj, key, value)
    
    def delete_attribute(self, obj, key, **kwargs):
        self.get_history(obj, key, **kwargs).delattr()
        self.value_changed(obj, key, None)
        
    def rollback(self, *obj):
        for o in obj:
            try:
                attributes = self.attribute_history(o)
                for hist in attributes.values():
                    hist.rollback()
            except KeyError:
                pass

    def commit(self, *obj):
        for o in obj:
            try:
                attributes = self.attribute_history(o)
                for hist in attributes.values():
                    hist.commit()
            except KeyError:
                pass
                
    def remove(self, obj):
        pass

    def create_history(self, obj, key, uselist, callable_=None, **kwargs):
        p = self.create_history_container(obj, key, uselist, callable_=callable_, **kwargs)
        self.attribute_history(obj)[key] = p
        return p

    def get_history(self, obj, key, passive=False, **kwargs):
        try:
            return self.attribute_history(obj)[key].gethistory(passive=passive, **kwargs)
        except KeyError, e:
            return self.class_managed(obj.__class__)[key](obj, **kwargs).gethistory(passive=passive, **kwargs)

    def attribute_history(self, obj):
        try:
            attr = obj.__dict__['_managed_attributes']
        except KeyError:
            attr = {}
            obj.__dict__['_managed_attributes'] = attr
        return attr

    def reset_history(self, obj, key):
        try:
            del self.attribute_history(obj)[key]
        except KeyError:
            pass
        
    def class_managed(self, class_):
        try:
            attr = getattr(class_, '_class_managed_attributes')
        except AttributeError:
            attr = {}
            class_._class_managed_attributes = attr
        return attr


    def create_history_container(self, obj, key, uselist, callable_ = None, **kwargs):
        if callable_ is not None:
            return self.create_callable(obj, key, callable_, uselist=uselist, **kwargs)
        elif not uselist:
            return PropHistory(obj, key, **kwargs)
        else:
            list_ = obj.__dict__.get(key, None)
            return self.create_list(obj, key, list_, **kwargs)
        
    def register_attribute(self, class_, key, uselist, callable_=None, **kwargs):
        # create a function, that will create this object attribute when its first called
        def createprop(obj):
            if callable_ is not None: 
                func = callable_(obj)
            else:
                func = None
            p = self.create_history_container(obj, key, uselist, callable_=func, **kwargs)
            self.attribute_history(obj)[key] = p
            return p
        
        self.class_managed(class_)[key] = createprop
        setattr(class_, key, self.create_prop(key, uselist))