1"""
2$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/durus/persistent.py $
3$Id: persistent.py 31094 2008-09-15 11:34:19Z dbinger $
4"""
5from durus.utils import str_to_int8, iteritems, as_bytes
6from sys import stderr
7
8# these must match the constants in _persistent.c
9UNSAVED = 1
10SAVED = 0
11GHOST = -1
12
13
14try:
15    from durus._persistent import PersistentBase, ConnectionBase
16    from durus._persistent import _setattribute, _delattribute
17    from durus._persistent import _getattribute, _hasattribute
18    from durus._persistent import call_if_persistent
19    [ConnectionBase, _hasattribute, call_if_persistent] # silence import checker
20except ImportError:
21    stderr.write('Using Python base classes for persistence.\n')
22
23    _setattribute = object.__setattr__
24    _delattribute = object.__delattr__
25    _getattribute = object.__getattribute__
26
27    def _hasattribute(obj, name):
28        try:
29            _getattribute(obj, name)
30        except AttributeError:
31            return False
32        else:
33            return True
34
35    class ConnectionBase(object):
36        """
37        The faster implementation of this class is in _persistent.c.
38        """
39
40        __slots__ = ['transaction_serial']
41
42        def __new__(klass, *args, **kwargs):
43            instance = object.__new__(klass)
44            instance.transaction_serial = 1
45            return instance
46
47
48    _GHOST_SAFE_ATTRIBUTES = {
49        '__repr__': 1,
50        '__class__': 1,
51        '__setstate__': 1,
52    }
53
54    class PersistentBase(object):
55        """
56        The faster implementation of this class is in _persistent.c.
57        The __slots__ and methods of this class are the ones that typical
58        applications use very frequently, so we want them to be fast.
59
60        Instance attributes:
61          _p_status: UNSAVED | SAVED | GHOST
62            UNSAVED means that state that is here, self.__dict__, is usable and
63              has not been stored.
64            SAVED means that the state that is here, self.__dict__, is usable
65              and the same as the stored state.
66            GHOST means that the state that is here, self.__dict__, is empty
67              and unusable until it is updated from storage.
68
69            New instances are UNSAVED.
70            UNSAVED -> SAVED
71              happens when the instance state, self.__dict__, is saved.
72            UNSAVED -> GHOST
73              happens on an abort (if the object has previously been saved).
74            SAVED   -> UNSAVED
75              happens when changes are made to self.__dict__.
76            SAVED   -> GHOST
77              happens when the cache manager wants space.
78            GHOST   -> SAVED
79              happens when the instance state is loaded from the storage.
80            GHOST   -> UNSAVED
81              this happens when you want to make changes to self.__dict__.
82              The stored state is loaded during this state transition.
83          _p_serial: int
84            On every access, this attribute is set to self._p_connection.serial
85            (if _p_connection is not None).
86          _p_connection: durus.connection.Connection | None
87            The Connection to the Storage that stores this instance.
88            The _p_connection is None when this instance has never been stored.
89          _p_oid: str | None
90            The identifier assigned when the instance was first stored.
91            The _p_oid is None when this instance has never been stored.
92        """
93
94        __slots__ = ['_p_status', '_p_serial', '_p_connection', '_p_oid']
95
96        def __new__(klass, *args, **kwargs):
97            instance = object.__new__(klass)
98            instance._p_status = UNSAVED
99            instance._p_serial = 0
100            instance._p_connection = None
101            instance._p_oid = None
102            return instance
103
104        def __getattribute__(self, name):
105            if name[:3] != '_p_' and name not in _GHOST_SAFE_ATTRIBUTES:
106                if self._p_status == GHOST:
107                    self._p_load_state()
108                connection = self._p_connection
109                if (connection is not None and
110                    self._p_serial != connection.transaction_serial):
111                    connection.note_access(self)
112            return _getattribute(self, name)
113
114        def __setattr__(self, name, value):
115            if name[:3] != '_p_' and name not in _GHOST_SAFE_ATTRIBUTES:
116                self._p_note_change()
117            _setattribute(self, name, value)
118
119    def call_if_persistent(f, x):
120        if isinstance(x, PersistentBase):
121            return f(x)
122        else:
123            return None
124
125
126class PersistentObject (PersistentBase):
127    """
128    All Durus persistent objects should inherit from this class.
129    """
130    __slots__ = ['__weakref__']
131
132    def _p_gen_data_slots(self):
133        """Generate the sequence of names of data slots that have values.
134        """
135        for klass in self.__class__.__mro__:
136            if klass is not PersistentBase:
137                for name in getattr(klass, '__slots__', []):
138                    if (name not in ('__weakref__', '__dict__') and
139                        _hasattribute(self, name)):
140                        yield name
141
142    def __getstate__(self):
143        if self._p_status == GHOST:
144            self._p_load_state()
145        state = {}
146        if _hasattribute(self, '__dict__'):
147            state.update(_getattribute(self, '__dict__'))
148        for name in self._p_gen_data_slots():
149            state[name] = _getattribute(self, name)
150        return state
151
152    def __setstate__(self, state):
153        if _hasattribute(self, '__dict__'):
154            _getattribute(self, '__dict__').clear()
155        for name in self._p_gen_data_slots():
156            _delattribute(self, name)
157        if state is not None:
158            for key, value in iteritems(state):
159                _setattribute(self, key, value)
160
161    def __repr__(self):
162        if self._p_oid is None:
163            identifier = '@%x' % id(self)
164        else:
165            identifier = self._p_format_oid()
166        return "<%s %s>" % (self.__class__.__name__, identifier)
167
168    def __delattr__(self, name):
169        self._p_note_change()
170        _delattribute(self, name)
171
172    def _p_load_state(self):
173        assert self._p_status == GHOST
174        self._p_connection.load_state(self)
175        self._p_set_status_saved()
176
177    def _p_note_change(self):
178        if self._p_status != UNSAVED:
179            self._p_set_status_unsaved()
180            self._p_connection.note_change(self)
181
182    def _p_format_oid(self):
183        oid = self._p_oid
184        return str(oid and str_to_int8(as_bytes(oid)))
185
186    def _p_set_status_ghost(self):
187        self.__setstate__({})
188        self._p_status = GHOST
189
190    def _p_set_status_saved(self):
191        self._p_status = SAVED
192
193    def _p_set_status_unsaved(self):
194        if self._p_status == GHOST:
195            self._p_load_state()
196        self._p_status = UNSAVED
197
198    def _p_is_ghost(self):
199        return self._p_status == GHOST
200
201    def _p_is_unsaved(self):
202        return self._p_status == UNSAVED
203
204    def _p_is_saved(self):
205        return self._p_status == SAVED
206
207
208class Persistent (PersistentObject):
209    """
210    This is the traditional persistent class of Durus.  The state is stored
211    in the __dict__.
212    """
213    def __getstate__(self):
214        return self.__dict__
215
216    def __setstate__(self, state):
217        self_dict = _getattribute(self, '__dict__')
218        self_dict.clear()
219        self_dict.update(state)
220
221
222class ComputedAttribute (PersistentObject):
223    """Computed attributes do not have any state that needs to be saved in
224    the database.  Instead, their value is computed based on other persistent
225    objects.  Although they have no real persistent state, they do have
226    OIDs.  That allows synchronize of the cached value between connections
227    (necessary to maintain consistency).  If the value becomes invalid in one
228    connection then it must be invalidated in all connections.  That is
229    achieved by marking the object as UNSAVED and treating it like a normal
230    persistent object.
231
232    Instance attributes: none
233    """
234    __slots__ = ['value']
235
236    def __getstate__(self):
237        return None
238
239    def _p_load_state(self):
240        # don't need to read state from connection, there is none
241        self._p_set_status_saved()
242
243    def invalidate(self):
244        """Forget value and mark object as UNSAVED.  On commit it will cause
245        other connections to receive a invalidation notification and forget the
246        value as well.
247        """
248        self.__setstate__(None)
249        self._p_note_change()
250
251    def get(self, compute):
252        """(compute) -> value
253
254        Compute the value (if necessary) and return it.  'compute' needs
255        to be a function that takes no arguments.
256        """
257        # we are careful here not to mark object as UNSAVED
258        if _hasattribute(self, 'value'):
259            value = _getattribute(self, 'value')
260        else:
261            value = compute()
262            _setattribute(self, 'value', value)
263        return value
264