Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/dev/saving_dict.py
8815 views
1
r"""
2
A dictionary which is automatically stored in the file system
3
4
This module provides a special dictionary class for :class:`sagedev.SageDev`
5
which is automatically written to the file system whenever it changes.
6
7
AUTHORS:
8
9
- David Roe, Julian Rueth, Robert Bradshaw: initial version
10
11
"""
12
#*****************************************************************************
13
# Copyright (C) 2013 David Roe <[email protected]>
14
# Julian Rueth <[email protected]>
15
# Robert Bradshaw <[email protected]>
16
#
17
# Distributed under the terms of the GNU General Public License (GPL)
18
# as published by the Free Software Foundation; either version 2 of
19
# the License, or (at your option) any later version.
20
# http://www.gnu.org/licenses/
21
#*****************************************************************************
22
import cPickle
23
from cStringIO import StringIO
24
import os
25
import collections
26
27
def _raise():
28
r"""
29
Helper method for :class:`SavingDict` which reraises an exception.
30
31
TESTS::
32
33
sage: from sage.dev.saving_dict import _raise
34
sage: try:
35
....: raise Exception("this is a test")
36
....: except Exception:
37
....: s = "the exception was caught"
38
....: _raise()
39
Traceback (most recent call last):
40
...
41
Exception: this is a test
42
sage: print s
43
the exception was caught
44
"""
45
raise
46
47
class SavingDict(collections.MutableMapping):
48
r"""
49
Dictionary-like class that saves itself on the filesystem.
50
51
INPUT:
52
53
- ``filename`` -- a string, file to store SavingDict
54
55
- ``values`` -- dictionary-like object that sets initial values or ``None``
56
(default: ``None``); by default initial values will be loaded from
57
filename, if it exists, otherwise the initial values will be empty.
58
59
- ``default`` -- callabable object requiring no arguments that provides a
60
default value for keys not in the :class:`SavingDict` (default:
61
``_raise``, which raises an exception if a key is not present)
62
63
- ``paired`` -- another :class:`SavingDict` or ``None`` (default:
64
``None``), this dictionary will be paired as per :meth:`set_paired`
65
66
EXAMPLES::
67
68
sage: from sage.dev.saving_dict import SavingDict
69
sage: tmp = tmp_filename()
70
sage: sd = SavingDict(tmp)
71
sage: sd['cat'] = 'meow'; sd['cat']
72
'meow'
73
sage: sd['cow'] = 'moo'; sd
74
{'cow': 'moo', 'cat': 'meow'}
75
sage: del sd; sd
76
Traceback (most recent call last):
77
...
78
NameError: name 'sd' is not defined
79
80
Creating another instance (with the same file name) loads the
81
cached values::
82
83
sage: sd = SavingDict(tmp); sd
84
{'cow': 'moo', 'cat': 'meow'}
85
sage: sd._erase()
86
"""
87
def __init__(self,
88
filename,
89
values=None,
90
default=_raise,
91
paired=None):
92
"""
93
Initialization.
94
95
EXAMPLES::
96
97
sage: from sage.dev.saving_dict import SavingDict
98
sage: sd = SavingDict(tmp_filename())
99
sage: sd['cat'] = 'meow'; sd
100
{'cat': 'meow'}
101
sage: sd._erase()
102
"""
103
if not callable(default):
104
raise ValueError("default must be callable")
105
106
self._filename = filename
107
if values is None:
108
self._dict = SavingDict.load_dict_from_file(filename)
109
else:
110
self._dict = dict(values) # explicitly make copy
111
self._default = default
112
self._pairing = None
113
if paired is not None:
114
self.set_paired(paired)
115
if not os.path.exists(self._filename):
116
self._write()
117
118
def __repr__(self):
119
r"""
120
Return a printable representation of this object.
121
122
TESTS::
123
124
sage: from sage.dev.saving_dict import SavingDict
125
sage: sd = SavingDict(tmp_filename(), {0:1, 1:2})
126
sage: repr(sd)
127
'{0: 1, 1: 2}'
128
sage: sd._erase()
129
"""
130
return repr(self._dict)
131
132
def unset_pairing(self):
133
r"""
134
Unset any pairing that was constructed with :meth:`set_paired`.
135
136
EXAMPLES::
137
138
sage: from sage.dev.saving_dict import SavingDict
139
sage: sd1 = SavingDict(tmp_filename())
140
sage: sd2 = SavingDict(tmp_filename(), paired=sd1)
141
sage: sd1[0] = 1; sd1[0]; sd2[1]
142
1
143
0
144
sage: sd1.unset_pairing()
145
sage: sd1._erase()
146
sage: del sd1[0]; sd1[0]
147
Traceback (most recent call last):
148
...
149
KeyError: 0
150
sage: sd2[1]
151
0
152
sage: sd2._erase()
153
"""
154
if self._pairing:
155
with self._pairing as paired:
156
paired.unset_pairing()
157
self._pairing = None
158
159
def set_paired(self, other):
160
r"""
161
Set another class:`SavingDict` to be updated with the reverse of this
162
one and vice versa.
163
164
EXAMPLES::
165
166
sage: from sage.dev.saving_dict import SavingDict
167
sage: sd1 = SavingDict(tmp_filename())
168
sage: sd2 = SavingDict(tmp_filename())
169
sage: sd1.set_paired(sd2)
170
sage: sd1[0] = 1; sd1[0]; sd2[1]
171
1
172
0
173
sage: del sd1[0]
174
sage: sd1[0]
175
Traceback (most recent call last):
176
...
177
KeyError: 0
178
sage: sd2[1]
179
Traceback (most recent call last):
180
...
181
KeyError: 1
182
sage: sd2[2] = 3; sd1[3]; sd2[2]
183
2
184
3
185
sage: sd2[2] = 4; sd1[3]
186
Traceback (most recent call last):
187
...
188
KeyError: 3
189
sage: sd1[4]; sd2[2]
190
2
191
4
192
sage: sd1._erase(); sd2._erase()
193
"""
194
if not isinstance(other, SavingDict):
195
raise ValueError("other is not a SavingDict")
196
197
self.unset_pairing()
198
other.unset_pairing()
199
200
class Pairing(object):
201
def __init__(this, obj):
202
this._obj = obj
203
def __enter__(this):
204
this._entered[0] += 1
205
return other if this._obj is self else self
206
def __exit__(this, type, value, tb):
207
this._entered[0] -= 1
208
def __nonzero__(this):
209
return not this._entered[0]
210
211
Pairing._entered = [0] # ints are immutable
212
213
self._pairing, other._pairing = Pairing(self), Pairing(other)
214
215
def _write(self):
216
r"""
217
Write this dictionary to disk.
218
219
EXAMPLES::
220
221
sage: from sage.dev.saving_dict import SavingDict
222
sage: tmp = tmp_filename()
223
sage: sd = SavingDict(tmp, {0:1, 1:2})
224
sage: SavingDict(tmp)
225
{}
226
sage: sd._write()
227
sage: SavingDict(tmp)
228
{0: 1, 1: 2}
229
sage: sd._erase()
230
"""
231
from sage.doctest import DOCTEST_MODE
232
if DOCTEST_MODE:
233
from sage.misc.misc import SAGE_TMP
234
SAGE_TMP = str(SAGE_TMP)
235
error = "write attempt to a saving_dict in a doctest: "+self._filename
236
assert os.path.abspath(self._filename).startswith(SAGE_TMP), error
237
238
import tempfile
239
dirname = os.path.dirname(os.path.abspath(self._filename))
240
fd, tmpfile = tempfile.mkstemp(dir=dirname)
241
s = cPickle.dumps(self._dict, protocol=2)
242
with os.fdopen(fd, "wb") as F:
243
F.write(s)
244
try:
245
# This move is atomic (the files are on the same filesystem)
246
os.rename(tmpfile, self._filename)
247
except (IOError, OSError):
248
# Lesser operation systems cannot do atomic moves (looking at you, windows)
249
os.unlink(self._filename)
250
os.rename(tmpfile, self._filename)
251
252
def _erase(self):
253
"""
254
Erase the backing file.
255
256
EXAMPLES::
257
258
sage: from sage.dev.saving_dict import SavingDict
259
sage: tmp = tmp_filename()
260
sage: sd = SavingDict(tmp, {0:1, 1:2})
261
sage: sd._write()
262
sage: os.path.exists(tmp)
263
True
264
sage: sd._erase()
265
sage: os.path.exists(tmp)
266
False
267
"""
268
os.unlink(self._filename)
269
270
def __setitem__(self, key, value):
271
r"""
272
Set ``key`` to ``value``.
273
274
TESTS::
275
276
sage: from sage.dev.saving_dict import SavingDict
277
sage: sd = SavingDict(tmp_filename())
278
sage: sd['cow'] = 'moo'
279
sage: sd
280
{'cow': 'moo'}
281
sage: sd._erase()
282
"""
283
if self._pairing:
284
with self._pairing as paired:
285
if key in self:
286
del paired[self[key]]
287
paired[value] = key
288
self._dict[key] = value
289
self._write()
290
291
def __delitem__(self, key):
292
r"""
293
Remove ``key`` from this dictionary.
294
295
TESTS::
296
297
sage: from sage.dev.saving_dict import SavingDict
298
sage: sd = SavingDict(tmp_filename(), {'cow': 'moo'}); sd
299
{'cow': 'moo'}
300
sage: del sd['cow']
301
sage: del sd['cow']
302
Traceback (most recent call last):
303
...
304
KeyError: 'cow'
305
sage: sd
306
{}
307
sage: sd._erase()
308
"""
309
if self._pairing and key in self:
310
with self._pairing as paired:
311
del paired[self[key]]
312
del self._dict[key]
313
self._write()
314
315
def __getitem__(self, key):
316
r"""
317
Return the value for ``key``.
318
319
TESTS::
320
321
sage: from sage.dev.saving_dict import SavingDict
322
sage: sd = SavingDict(tmp_filename(), {'cow': 'moo'}); sd
323
{'cow': 'moo'}
324
sage: sd['cow']
325
'moo'
326
sage: sd['moo']
327
Traceback (most recent call last):
328
...
329
KeyError: 'moo'
330
sage: sd._erase()
331
"""
332
try:
333
return self._dict[key]
334
except KeyError:
335
return self._default()
336
337
def __contains__(self, key):
338
r"""
339
Return whether this dictionary contains ``key``.
340
341
TESTS::
342
343
sage: from sage.dev.saving_dict import SavingDict
344
sage: sd = SavingDict(tmp_filename(), {'cow': 'moo'}); sd
345
{'cow': 'moo'}
346
sage: 'cow' in sd
347
True
348
sage: 'moo' in sd
349
False
350
sage: sd._erase()
351
"""
352
return key in self._dict
353
354
def __len__(self):
355
r"""
356
Return the number of keys in this dictionary.
357
358
TESTS::
359
360
sage: from sage.dev.saving_dict import SavingDict
361
sage: sd = SavingDict(tmp_filename()); len(sd)
362
0
363
sage: sd['cow'] = 'moo'
364
sage: len(sd)
365
1
366
sage: sd._erase()
367
"""
368
return len(self._dict)
369
370
def __iter__(self):
371
r"""
372
Return an iterator over the keys of this dictionary.
373
374
TESTS::
375
376
sage: from sage.dev.saving_dict import SavingDict
377
sage: sd = SavingDict(tmp_filename(), {'cow':'moo', 0:1}); sd
378
{0: 1, 'cow': 'moo'}
379
sage: for key in sd:
380
... print key, sd[key]
381
0 1
382
cow moo
383
sage: sd._erase()
384
"""
385
return iter(self._dict)
386
387
@classmethod
388
def load_dict_from_file(cls, filename):
389
r"""
390
Load a pickled dictionary from ``filename``, defaults to {} if the file
391
does not exist.
392
393
TESTS::
394
395
sage: from sage.dev.saving_dict import SavingDict
396
sage: d = SavingDict.load_dict_from_file(''); d
397
{}
398
sage: d['cow'] = 'moo'
399
sage: import cPickle
400
sage: tmp = tmp_filename()
401
sage: with open(tmp, 'w') as f:
402
....: f.write(cPickle.dumps(d, protocol=2))
403
sage: SavingDict.load_dict_from_file(tmp)
404
{'cow': 'moo'}
405
sage: os.unlink(tmp)
406
"""
407
if os.path.exists(filename):
408
with open(filename) as F:
409
s = F.read()
410
if s:
411
unpickler = cPickle.Unpickler(StringIO(s))
412
try:
413
return unpickler.load()
414
except:
415
# catch-all exception! Unpickling can cause all
416
# kinds of exceptions, e.g. AttributeError if the
417
# Sage source code changed.
418
pass
419
return {}
420
421