1
|
|
###############################################################################
|
2
|
|
# pyrpl - DSP servo controller for quantum optics with the RedPitaya
|
3
|
|
# Copyright (C) 2014-2016 Leonhard Neuhaus (neuhaus@spectro.jussieu.fr)
|
4
|
|
#
|
5
|
|
# This program is free software: you can redistribute it and/or modify
|
6
|
|
# it under the terms of the GNU General Public License as published by
|
7
|
|
# the Free Software Foundation, either version 3 of the License, or
|
8
|
|
# (at your option) any later version.
|
9
|
|
#
|
10
|
|
# This program is distributed in the hope that it will be useful,
|
11
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
|
# GNU General Public License for more details.
|
14
|
|
#
|
15
|
|
# You should have received a copy of the GNU General Public License
|
16
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
|
###############################################################################
|
18
|
|
|
19
|
3
|
import os
|
20
|
3
|
from collections import OrderedDict
|
21
|
3
|
from shutil import copyfile
|
22
|
3
|
import numpy as np
|
23
|
3
|
import time
|
24
|
3
|
from qtpy import QtCore
|
25
|
3
|
from . import default_config_dir, user_config_dir
|
26
|
3
|
from .pyrpl_utils import time
|
27
|
|
|
28
|
3
|
import logging
|
29
|
3
|
logger = logging.getLogger(name=__name__)
|
30
|
|
|
31
|
|
|
32
|
3
|
class UnexpectedSaveError(RuntimeError):
|
33
|
3
|
pass
|
34
|
|
# the config file is read through a yaml interface. The preferred one is
|
35
|
|
# ruamel.yaml, since it allows to preserve comments and whitespace in the
|
36
|
|
# config file through roundtrips (the config file is rewritten every time a
|
37
|
|
# parameter is changed). If ruamel.yaml is not installed, the program will
|
38
|
|
# issue a warning and use pyyaml (=yaml= instead). Comments are lost in this
|
39
|
|
# case.
|
40
|
3
|
try:
|
41
|
3
|
raise # disables ruamel support
|
42
|
|
|
43
|
0
|
import ruamel.yaml
|
44
|
|
#ruamel.yaml.add_implicit_resolver()
|
45
|
0
|
ruamel.yaml.RoundTripDumper.add_representer(np.float64,
|
46
|
|
lambda dumper, data: dumper.represent_float(float(data)))
|
47
|
0
|
ruamel.yaml.RoundTripDumper.add_representer(complex,
|
48
|
|
lambda dumper, data: dumper.represent_str(str(data)))
|
49
|
0
|
ruamel.yaml.RoundTripDumper.add_representer(np.complex128,
|
50
|
|
lambda dumper, data: dumper.represent_str(str(data)))
|
51
|
0
|
ruamel.yaml.RoundTripDumper.add_representer(np.ndarray,
|
52
|
|
lambda dumper, data: dumper.represent_list(list(data)))
|
53
|
|
|
54
|
|
#http://stackoverflow.com/questions/13518819/avoid-references-in-pyyaml
|
55
|
|
#ruamel.yaml.RoundTripDumper.ignore_aliases = lambda *args: True
|
56
|
0
|
def load(f):
|
57
|
0
|
return ruamel.yaml.load(f, ruamel.yaml.RoundTripLoader)
|
58
|
0
|
def save(data, stream=None):
|
59
|
0
|
return ruamel.yaml.dump(data, stream=stream,
|
60
|
|
Dumper=ruamel.yaml.RoundTripDumper,
|
61
|
|
default_flow_style=False)
|
62
|
3
|
except:
|
63
|
3
|
logger.debug("ruamel.yaml could not be imported. Using yaml instead. "
|
64
|
|
"Comments in config files will be lost.")
|
65
|
3
|
import yaml
|
66
|
|
|
67
|
|
# see http://stackoverflow.com/questions/13518819/avoid-references-in-pyyaml
|
68
|
|
#yaml.Dumper.ignore_aliases = lambda *args: True # NEVER TESTED
|
69
|
|
|
70
|
|
# ordered load and dump for yaml files. From
|
71
|
|
# http://stackoverflow.com/questions/5121931/in-python-how-can-you-load-yaml-mappings-as-ordereddicts
|
72
|
3
|
def load(stream, Loader=yaml.SafeLoader, object_pairs_hook=OrderedDict):
|
73
|
3
|
class OrderedLoader(Loader):
|
74
|
3
|
pass
|
75
|
3
|
def construct_mapping(loader, node):
|
76
|
3
|
loader.flatten_mapping(node)
|
77
|
3
|
return object_pairs_hook(loader.construct_pairs(node))
|
78
|
3
|
OrderedLoader.add_constructor(
|
79
|
|
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
80
|
|
construct_mapping)
|
81
|
3
|
return yaml.load(stream, OrderedLoader)
|
82
|
3
|
def save(data, stream=None, Dumper=yaml.SafeDumper,
|
83
|
|
default_flow_style=False,
|
84
|
|
encoding='utf-8',
|
85
|
|
**kwds):
|
86
|
3
|
class OrderedDumper(Dumper):
|
87
|
3
|
pass
|
88
|
3
|
def _dict_representer(dumper, data):
|
89
|
3
|
return dumper.represent_mapping(
|
90
|
|
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
91
|
|
data.items())
|
92
|
3
|
OrderedDumper.add_representer(OrderedDict, _dict_representer)
|
93
|
3
|
OrderedDumper.add_representer(np.float64,
|
94
|
|
lambda dumper, data: dumper.represent_float(float(data)))
|
95
|
3
|
OrderedDumper.add_representer(complex,
|
96
|
|
lambda dumper, data: dumper.represent_str(str(data)))
|
97
|
3
|
OrderedDumper.add_representer(np.complex128,
|
98
|
|
lambda dumper, data: dumper.represent_str(str(data)))
|
99
|
3
|
OrderedDumper.add_representer(np.ndarray,
|
100
|
|
lambda dumper, data: dumper.represent_list(list(data)))
|
101
|
|
# I added the following two lines to make pyrpl compatible with pyinstruments. In principle they can be erased
|
102
|
3
|
if isinstance(data, dict) and not isinstance(data, OrderedDict):
|
103
|
0
|
data = OrderedDict(data)
|
104
|
3
|
return yaml.dump(data,
|
105
|
|
stream=stream,
|
106
|
|
Dumper=OrderedDumper,
|
107
|
|
default_flow_style=default_flow_style,
|
108
|
|
encoding=encoding,
|
109
|
|
**kwds)
|
110
|
|
|
111
|
|
# usage example:
|
112
|
|
# load(stream, yaml.SafeLoader)
|
113
|
|
# save(data, stream=f, Dumper=yaml.SafeDumper)
|
114
|
|
|
115
|
|
|
116
|
3
|
def isbranch(obj):
|
117
|
3
|
return isinstance(obj, dict) or isinstance(obj, list)
|
118
|
|
|
119
|
|
|
120
|
|
# two functions to locate config files
|
121
|
3
|
def _get_filename(filename=None):
|
122
|
|
""" finds the correct path and name of a config file """
|
123
|
|
# accidentally, we may pass a MemoryTree object instead of file
|
124
|
3
|
if isinstance(filename, MemoryTree):
|
125
|
0
|
return filename._filename
|
126
|
|
# get extension right
|
127
|
3
|
if not filename.endswith(".yml"):
|
128
|
3
|
filename = filename + ".yml"
|
129
|
|
# see if filename is found with given path, or in user_config or in default_config
|
130
|
3
|
p, f = os.path.split(filename)
|
131
|
3
|
for path in [p, user_config_dir, default_config_dir]:
|
132
|
3
|
file = os.path.join(path, f)
|
133
|
3
|
if os.path.isfile(file):
|
134
|
3
|
return file
|
135
|
|
# file not existing, place it in user_config_dir
|
136
|
3
|
return os.path.join(user_config_dir, f)
|
137
|
|
|
138
|
|
|
139
|
3
|
def get_config_file(filename=None, source=None):
|
140
|
|
""" returns the path to a valid, existing config file with possible source specification """
|
141
|
|
# if None is specified, that means we do not want a persistent config file
|
142
|
3
|
if filename is None:
|
143
|
3
|
return filename
|
144
|
|
# try to locate the file
|
145
|
3
|
filename = _get_filename(filename)
|
146
|
3
|
if os.path.isfile(filename): # found a file
|
147
|
3
|
p, f = os.path.split(filename)
|
148
|
3
|
if p == default_config_dir:
|
149
|
|
# check whether path is default_config_dir and make a copy in
|
150
|
|
# user_config_dir in order to not alter original files
|
151
|
3
|
dest = os.path.join(user_config_dir, f)
|
152
|
3
|
copyfile(filename, dest)
|
153
|
3
|
return dest
|
154
|
|
else:
|
155
|
3
|
return filename
|
156
|
|
# file not existing, try to get it from source
|
157
|
3
|
if source is not None:
|
158
|
3
|
source = _get_filename(source)
|
159
|
3
|
if os.path.isfile(source): # success - copy the source
|
160
|
3
|
logger.debug("File " + filename + " not found. New file created from source '%s'. "%source)
|
161
|
3
|
copyfile(source,filename)
|
162
|
3
|
return filename
|
163
|
|
# still not returned -> create empty file
|
164
|
3
|
with open(filename, mode="w"):
|
165
|
3
|
pass
|
166
|
3
|
logger.debug("File " + filename + " not found. New file created. ")
|
167
|
3
|
return filename
|
168
|
|
|
169
|
|
|
170
|
3
|
class MemoryBranch(object):
|
171
|
|
"""Represents a branch of a memoryTree
|
172
|
|
|
173
|
|
All methods are preceded by an underscore to guarantee that tab
|
174
|
|
expansion of a memory branch only displays the available subbranches or
|
175
|
|
leaves. A memory tree is a hierarchical structure. Nested dicts are
|
176
|
|
interpreted as subbranches.
|
177
|
|
|
178
|
|
Parameters
|
179
|
|
----------
|
180
|
|
parent: MemoryBranch
|
181
|
|
parent is the parent MemoryBranch
|
182
|
|
branch: str
|
183
|
|
branch is a string with the name of the branch to create
|
184
|
|
defaults: list
|
185
|
|
list of default branches that are used if requested data is not
|
186
|
|
found in the current branch
|
187
|
|
|
188
|
|
Class members
|
189
|
|
-----------
|
190
|
|
all properties without preceeding underscore are config file entries
|
191
|
|
|
192
|
|
_data: the raw data underlying the branch. Type depends on the
|
193
|
|
loader and can be dict, OrderedDict or CommentedMap
|
194
|
|
_dict: similar to _data, but the dict contains all default
|
195
|
|
branches
|
196
|
|
_defaults: list of MemoryBranch objects in order of decreasing
|
197
|
|
priority that are used as defaults for the Branch.
|
198
|
|
Changing the default values from the software will replace
|
199
|
|
the default values in the current MemoryBranch but not
|
200
|
|
alter the underlying default branch. Changing the
|
201
|
|
default branch when it is not overridden by the current
|
202
|
|
MemoryBranch results in an effective change in the branch.
|
203
|
|
_keys: same as _dict._keys()
|
204
|
|
_update: updates the branch with another dict
|
205
|
|
_pop: removes a value/subbranch from the branch
|
206
|
|
_root: the MemoryTree object (root) of the tree
|
207
|
|
_parent: the parent of the branch
|
208
|
|
_branch: the name of the branch
|
209
|
|
_get_or_create: creates a new branch and returns it. Same as branch[newname]=dict(), but also supports nesting,
|
210
|
|
e.g. newname="lev1.lev2.level3"
|
211
|
|
_fullbranchname: returns the full path from root to the branch
|
212
|
|
_getbranch: returns a branch by specifying its path, e.g. 'b1.c2.d3'
|
213
|
|
_rename: renames the branch
|
214
|
|
_reload: attempts to reload the data from disc
|
215
|
|
_save: attempts to save the data to disc
|
216
|
|
|
217
|
|
If a subbranch or a value is requested but does not exist in the current MemoryTree, a KeyError is raised.
|
218
|
|
"""
|
219
|
|
|
220
|
3
|
def __init__(self, parent, branch):
|
221
|
3
|
self._parent = parent
|
222
|
3
|
self._branch = branch
|
223
|
3
|
self._update_instance_dict()
|
224
|
|
|
225
|
3
|
def _update_instance_dict(self):
|
226
|
3
|
data = self._data
|
227
|
3
|
if isinstance(data, dict):
|
228
|
3
|
for k in self.__dict__.keys():
|
229
|
3
|
if k not in data and not k.startswith('_'):
|
230
|
0
|
self.__dict__.pop(k)
|
231
|
3
|
for k in data.keys():
|
232
|
|
# write None since this is only a
|
233
|
|
# placeholder (__getattribute__ is overwritten below)
|
234
|
3
|
self.__dict__[k] = None
|
235
|
|
|
236
|
3
|
@property
|
237
|
|
def _data(self):
|
238
|
|
""" The raw data (OrderedDict) or Mapping of the branch """
|
239
|
3
|
return self._parent._data[self._branch]
|
240
|
|
|
241
|
3
|
@_data.setter
|
242
|
|
def _data(self, value):
|
243
|
0
|
logger.warning("You are directly modifying the data of MemoryBranch"
|
244
|
|
" %s to %s.", self._fullbranchname, str(value))
|
245
|
0
|
self._parent._data[self._branch] = value
|
246
|
|
|
247
|
3
|
def _keys(self):
|
248
|
3
|
if isinstance(self._data, list):
|
249
|
0
|
return range(self.__len__())
|
250
|
|
else:
|
251
|
3
|
return self._data.keys()
|
252
|
|
|
253
|
3
|
def _update(self, new_dict):
|
254
|
3
|
if isinstance(self._data, list):
|
255
|
0
|
raise NotImplementedError
|
256
|
3
|
self._data.update(new_dict)
|
257
|
3
|
self._save()
|
258
|
|
# keep auto_completion up to date
|
259
|
3
|
for k in new_dict:
|
260
|
0
|
self.__dict__[k] = None
|
261
|
|
|
262
|
3
|
def __getattribute__(self, name):
|
263
|
|
""" implements the dot notation.
|
264
|
|
Example: self.subbranch.leaf returns the item 'leaf' of 'subbranch' """
|
265
|
3
|
if name.startswith('_'):
|
266
|
3
|
return super(MemoryBranch, self).__getattribute__(name)
|
267
|
|
else:
|
268
|
|
# convert dot notation into dict notation
|
269
|
3
|
return self[name]
|
270
|
|
|
271
|
3
|
def __getitem__(self, item):
|
272
|
|
"""
|
273
|
|
__getitem__ bypasses the higher-level __getattribute__ function and provides
|
274
|
|
direct low-level access to the underlying dictionary.
|
275
|
|
This is much faster, as long as no changes have been made to the config
|
276
|
|
file.
|
277
|
|
"""
|
278
|
3
|
self._reload()
|
279
|
|
# if a subbranch is requested, iterate through the hierarchy
|
280
|
3
|
if isinstance(item, str) and '.' in item:
|
281
|
0
|
item, subitem = item.split('.', 1)
|
282
|
0
|
return self[item][subitem]
|
283
|
|
else: # otherwise just return what we can find
|
284
|
3
|
attribute = self._data[item] # read from the data dict
|
285
|
3
|
if isbranch(attribute): # if the object can be expressed as a branch, do so
|
286
|
3
|
return MemoryBranch(self, item)
|
287
|
|
else: # otherwise return whatever we found in the data dict
|
288
|
3
|
return attribute
|
289
|
|
|
290
|
3
|
def __setattr__(self, name, value):
|
291
|
3
|
if name.startswith('_'):
|
292
|
3
|
super(MemoryBranch, self).__setattr__(name, value)
|
293
|
|
else: # implemment dot notation
|
294
|
3
|
self[name] = value
|
295
|
|
|
296
|
3
|
def __setitem__(self, item, value):
|
297
|
|
"""
|
298
|
|
creates a new entry, overriding the protection provided by dot notation
|
299
|
|
if the value of this entry is of type dict, it becomes a MemoryBranch
|
300
|
|
new values can be added to the branch in the same manner
|
301
|
|
"""
|
302
|
|
# if the subbranch is set or replaced, to this in a specific way
|
303
|
3
|
if isbranch(value):
|
304
|
|
# naive way: self._data[item] = dict(value)
|
305
|
|
# rather: replace values in their natural order (e.g. if value is OrderedDict)
|
306
|
|
# make an empty subbranch
|
307
|
3
|
if isinstance(value, list):
|
308
|
3
|
self._set_data(item, [])
|
309
|
3
|
subbranch = self[item]
|
310
|
|
# use standard setter to set the values 1 by 1 and possibly as subbranch objects
|
311
|
3
|
for k, v in enumerate(value):
|
312
|
3
|
subbranch[k] = v
|
313
|
|
else: # dict-like
|
314
|
|
# makes an empty subbranch
|
315
|
3
|
self._set_data(item, dict())
|
316
|
3
|
subbranch = self[item]
|
317
|
|
# use standard setter to set the values 1 by 1 and possibly as subbranch objects
|
318
|
3
|
for k, v in value.items():
|
319
|
3
|
subbranch[k] = v
|
320
|
|
#otherwise just write to the data dictionary
|
321
|
|
else:
|
322
|
3
|
self._set_data(item, value)
|
323
|
3
|
if self._root._WARNING_ON_SAVE or self._root._ERROR_ON_SAVE:
|
324
|
0
|
logger.warning("Issuing call to MemoryTree._save after %s.%s=%s",
|
325
|
|
self._branch, item, value)
|
326
|
3
|
self._save()
|
327
|
|
# update the __dict__ for autocompletion
|
328
|
3
|
self.__dict__[item] = None
|
329
|
|
|
330
|
3
|
def _set_data(self, item, value):
|
331
|
|
"""
|
332
|
|
helper function to manage setting list entries that do not exist
|
333
|
|
"""
|
334
|
3
|
if isinstance(self._data, list) and item == len(self._data):
|
335
|
3
|
self._data.append(value)
|
336
|
|
else:
|
337
|
|
# trivial case: _data is dict or item within list length
|
338
|
|
# and we can simply set the entry
|
339
|
3
|
self._data[item] = value
|
340
|
|
|
341
|
3
|
def _pop(self, name):
|
342
|
|
"""
|
343
|
|
remove an item from the branch
|
344
|
|
"""
|
345
|
3
|
value = self._data.pop(name)
|
346
|
3
|
if name in self.__dict__.keys():
|
347
|
0
|
self.__dict__.pop(name)
|
348
|
3
|
self._save()
|
349
|
3
|
return value
|
350
|
|
|
351
|
3
|
def _rename(self, name):
|
352
|
0
|
self._parent[name] = self._parent._pop(self._branch)
|
353
|
0
|
self._save()
|
354
|
|
|
355
|
3
|
def _get_or_create(self, name):
|
356
|
|
"""
|
357
|
|
creates a new subbranch with name=name if it does not exist already
|
358
|
|
and returns it. If name is a branch hierarchy such as
|
359
|
|
"subbranch1.subbranch2.subbranch3", all three subbranch levels
|
360
|
|
are created
|
361
|
|
"""
|
362
|
3
|
if isinstance(name, int):
|
363
|
0
|
if name == 0 and len(self) == 0:
|
364
|
|
# instantiate a new list - odd way because we must
|
365
|
0
|
self._parent._data[self._branch] = []
|
366
|
|
# if index <= len, creation is done automatically if needed
|
367
|
|
# otherwise an error is raised
|
368
|
0
|
if name >= len(self):
|
369
|
0
|
self[name] = dict()
|
370
|
0
|
return self[name]
|
371
|
|
else: # dict-like subbranch, support several sublevels separated by '.'
|
372
|
|
# chop name into parts and iterate through them
|
373
|
3
|
currentbranch = self
|
374
|
3
|
for subbranchname in name.split("."):
|
375
|
|
# make new branch if applicable
|
376
|
3
|
if subbranchname not in currentbranch._data.keys():
|
377
|
3
|
currentbranch[subbranchname] = dict()
|
378
|
|
# move into new branch in case another subbranch will be created
|
379
|
3
|
currentbranch = currentbranch[subbranchname]
|
380
|
3
|
return currentbranch
|
381
|
|
|
382
|
3
|
def _erase(self):
|
383
|
|
"""
|
384
|
|
Erases the current branch
|
385
|
|
"""
|
386
|
0
|
self._parent._pop(self._branch)
|
387
|
0
|
self._save()
|
388
|
|
|
389
|
3
|
@property
|
390
|
|
def _root(self):
|
391
|
|
"""
|
392
|
|
returns the parent highest in hierarchy (the MemoryTree object)
|
393
|
|
"""
|
394
|
3
|
parent = self
|
395
|
3
|
while parent != parent._parent:
|
396
|
3
|
parent = parent._parent
|
397
|
3
|
return parent
|
398
|
|
|
399
|
3
|
@property
|
400
|
|
def _fullbranchname(self):
|
401
|
0
|
parent = self._parent
|
402
|
0
|
branchname = self._branch
|
403
|
0
|
while parent != parent._parent:
|
404
|
0
|
branchname = parent._branch + '.' + branchname
|
405
|
0
|
parent = parent._parent
|
406
|
0
|
return branchname
|
407
|
|
|
408
|
3
|
def _reload(self):
|
409
|
|
""" reload data from file"""
|
410
|
3
|
self._parent._reload()
|
411
|
|
|
412
|
3
|
def _save(self):
|
413
|
|
""" write data to file"""
|
414
|
3
|
self._parent._save()
|
415
|
|
|
416
|
3
|
def _get_yml(self, data=None):
|
417
|
|
"""
|
418
|
|
:return: returns the yml code for this branch
|
419
|
|
"""
|
420
|
0
|
return save(self._data if data is None else data).decode('utf-8')
|
421
|
|
|
422
|
3
|
def _set_yml(self, yml_content):
|
423
|
|
"""
|
424
|
|
:param yml_content: sets the branch to yml_content
|
425
|
|
:return: None
|
426
|
|
"""
|
427
|
0
|
branch = load(yml_content)
|
428
|
0
|
self._parent._data[self._branch] = branch
|
429
|
0
|
self._save()
|
430
|
|
|
431
|
3
|
def __len__(self):
|
432
|
3
|
return len(self._data)
|
433
|
|
|
434
|
3
|
def __contains__(self, item):
|
435
|
0
|
return item in self._data
|
436
|
|
|
437
|
3
|
def __repr__(self):
|
438
|
0
|
return "MemoryBranch(" + str(self._keys()) + ")"
|
439
|
|
|
440
|
3
|
def __add__(self, other):
|
441
|
|
"""
|
442
|
|
makes it possible to add list-like memory tree to a list
|
443
|
|
"""
|
444
|
3
|
if not isinstance(self._data, list):
|
445
|
0
|
raise NotImplementedError
|
446
|
3
|
return self._data + other
|
447
|
|
|
448
|
3
|
def __radd__(self, other):
|
449
|
|
"""
|
450
|
|
makes it possible to add list-like memory tree to a list
|
451
|
|
"""
|
452
|
3
|
if not isinstance(self._data, list):
|
453
|
0
|
raise NotImplementedError
|
454
|
3
|
return other + self._data
|
455
|
|
|
456
|
|
|
457
|
3
|
class MemoryTree(MemoryBranch):
|
458
|
|
"""
|
459
|
|
The highest level of a MemoryBranch construct. All attributes of this
|
460
|
|
object that do not start with '_' are other MemoryBranch objects or
|
461
|
|
Leaves, i.e. key - value pairs.
|
462
|
|
|
463
|
|
Parameters
|
464
|
|
----------
|
465
|
|
filename: str
|
466
|
|
The filename of the .yml file defining the MemoryTree structure.
|
467
|
|
"""
|
468
|
|
##### internal load logic:
|
469
|
|
# 1. initially, call _load() to get the data from the file
|
470
|
|
# 2. upon each inquiry of the config data, _reload() is called to
|
471
|
|
# ensure data integrity
|
472
|
|
# 3. _reload assumes a delay of _loadsavedeadtime between changing the
|
473
|
|
# config file and Pyrpl requesting the new data. That means, _reload
|
474
|
|
# will not attempt to touch the config file more often than every
|
475
|
|
# _loadsavedeadtime. The last interaction time with the file system is
|
476
|
|
# saved in the variable _lastreload. If this time is far enough in the
|
477
|
|
# past, the modification time of the config file is compared to _mtime,
|
478
|
|
# the internal memory of the last modifiation time by pyrpl. If the two
|
479
|
|
# don't match, the file was altered outside the scope of pyrpl and _load
|
480
|
|
# is called to reload it.
|
481
|
|
|
482
|
|
##### internal save logic:
|
483
|
|
|
484
|
|
# this structure will hold the data. Must define it here as immutable
|
485
|
|
# to overwrite the property _data of MemoryBranch
|
486
|
3
|
_data = None
|
487
|
|
|
488
|
3
|
_WARNING_ON_SAVE = False # flag that is used to debug excessive calls to
|
489
|
|
# save
|
490
|
3
|
_ERROR_ON_SAVE = False # Set this flag to true to raise
|
491
|
|
# Exceptions upon save
|
492
|
|
|
493
|
3
|
def __init__(self, filename=None, source=None, _loadsavedeadtime=3.0):
|
494
|
|
# never reload or save more frequently than _loadsavedeadtime because
|
495
|
|
# this is the principal cause of slowing down the code (typ. 30-200 ms)
|
496
|
|
# for immediate saving, call _save_now, for immediate loading _load_now
|
497
|
3
|
self._loadsavedeadtime = _loadsavedeadtime
|
498
|
|
# first, make sure filename exists
|
499
|
3
|
self._filename = get_config_file(filename, source)
|
500
|
3
|
if filename is None:
|
501
|
|
# to simulate a config file, only store data in memory
|
502
|
3
|
self._filename = filename
|
503
|
3
|
self._data = OrderedDict()
|
504
|
3
|
self._lastsave = time()
|
505
|
|
# create a timer to postpone to frequent savings
|
506
|
3
|
self._savetimer = QtCore.QTimer()
|
507
|
3
|
self._savetimer.setInterval(self._loadsavedeadtime*1000)
|
508
|
3
|
self._savetimer.setSingleShot(True)
|
509
|
3
|
self._savetimer.timeout.connect(self._write_to_file)
|
510
|
3
|
self._load()
|
511
|
|
|
512
|
3
|
self._save_counter = 0 # cntr for unittest and debug purposes
|
513
|
3
|
self._write_to_file_counter = 0 # cntr for unittest and debug purposes
|
514
|
|
|
515
|
|
# root of the tree is also a MemoryBranch with parent self and
|
516
|
|
# branch name ""
|
517
|
3
|
super(MemoryTree, self).__init__(self, "")
|
518
|
|
|
519
|
3
|
@property
|
520
|
|
def _buffer_filename(self):
|
521
|
|
""" makes a temporary file to ensure modification of config file is atomic (double-buffering like operation...)"""
|
522
|
3
|
return self._filename + '.tmp'
|
523
|
|
|
524
|
3
|
def _load(self):
|
525
|
|
""" loads data from file """
|
526
|
3
|
if self._filename is None:
|
527
|
|
# if no file is used, just ignore this call
|
528
|
3
|
return
|
529
|
3
|
logger.debug("Loading config file %s", self._filename)
|
530
|
|
# read file from disc
|
531
|
3
|
with open(self._filename) as f:
|
532
|
3
|
self._data = load(f)
|
533
|
|
# store the modification time of this file version
|
534
|
3
|
self._mtime = os.path.getmtime(self._filename)
|
535
|
|
# make sure that reload timeout starts from this moment
|
536
|
3
|
self._lastreload = time()
|
537
|
|
# empty file gives _data=None
|
538
|
3
|
if self._data is None:
|
539
|
3
|
self._data = OrderedDict()
|
540
|
|
# update dict of the MemoryTree object
|
541
|
3
|
to_remove = []
|
542
|
|
# remove all obsolete entries
|
543
|
3
|
for name in self.__dict__:
|
544
|
3
|
if not name.startswith('_') and name not in self._data:
|
545
|
0
|
to_remove.append(name)
|
546
|
3
|
for name in to_remove:
|
547
|
0
|
self.__dict__.pop(name)
|
548
|
|
# insert the branches into the object __dict__ for auto-completion
|
549
|
3
|
self.__dict__.update(self._data)
|
550
|
|
|
551
|
3
|
def _reload(self):
|
552
|
|
"""
|
553
|
|
reloads data from file if file has changed recently
|
554
|
|
"""
|
555
|
|
# first check if a reload was not performed recently (speed up reasons)
|
556
|
3
|
if self._filename is None:
|
557
|
3
|
return
|
558
|
|
# check whether reload timeout has expired
|
559
|
3
|
if time() > self._lastreload + self._loadsavedeadtime:
|
560
|
|
# prepare next timeout
|
561
|
3
|
self._lastreload = time()
|
562
|
3
|
logger.debug("Checking change time of config file...")
|
563
|
3
|
if self._mtime != os.path.getmtime(self._filename):
|
564
|
3
|
logger.debug("Loading because mtime %s != filetime %s",
|
565
|
|
self._mtime)
|
566
|
3
|
self._load()
|
567
|
|
else:
|
568
|
3
|
logger.debug("... no reloading required")
|
569
|
|
|
570
|
3
|
def _write_to_file(self):
|
571
|
|
"""
|
572
|
|
Immmediately writes the content of the memory tree to file
|
573
|
|
"""
|
574
|
|
# stop save timer
|
575
|
3
|
if hasattr(self, '_savetimer') and self._savetimer.isActive():
|
576
|
3
|
self._savetimer.stop()
|
577
|
3
|
self._lastsave = time()
|
578
|
3
|
self._write_to_file_counter += 1
|
579
|
3
|
logger.debug("Saving config file %s", self._filename)
|
580
|
3
|
if self._filename is None:
|
581
|
|
# skip writing to file if no filename was selected
|
582
|
3
|
return
|
583
|
|
else:
|
584
|
3
|
if self._mtime != os.path.getmtime(self._filename):
|
585
|
3
|
logger.warning("Config file has recently been changed on your " +
|
586
|
|
"harddisk. These changes might have been " +
|
587
|
|
"overwritten now.")
|
588
|
|
# we must be sure that overwriting config file never destroys existing data.
|
589
|
|
# security 1: backup with copyfile above
|
590
|
3
|
copyfile(self._filename,
|
591
|
|
self._filename + ".bak") # maybe this line is obsolete (see below)
|
592
|
|
# security 2: atomic writing such as shown in
|
593
|
|
# http://stackoverflow.com/questions/2333872/atomic-writing-to-file-with-python:
|
594
|
3
|
try:
|
595
|
3
|
f = open(self._buffer_filename, mode='w')
|
596
|
3
|
save(self._data, stream=f)
|
597
|
3
|
f.flush()
|
598
|
3
|
os.fsync(f.fileno())
|
599
|
3
|
f.close()
|
600
|
3
|
os.unlink(self._filename)
|
601
|
3
|
os.rename(self._buffer_filename, self._filename)
|
602
|
0
|
except:
|
603
|
0
|
copyfile(self._filename + ".bak", self._filename)
|
604
|
0
|
logger.error("Error writing to file. Backup version was restored.")
|
605
|
0
|
raise
|
606
|
|
# save last modification time of the file
|
607
|
3
|
self._mtime = os.path.getmtime(self._filename)
|
608
|
|
|
609
|
3
|
def _save(self, deadtime=None):
|
610
|
|
"""
|
611
|
|
A call to this function means that the state of the tree has changed
|
612
|
|
and needs to be saved eventually. To reduce system load, the delay
|
613
|
|
between two writes will be at least deadtime (defaults to
|
614
|
|
self._loadsavedeadtime if None)
|
615
|
|
"""
|
616
|
3
|
if self._ERROR_ON_SAVE:
|
617
|
0
|
raise UnexpectedSaveError("Save to config file should not "
|
618
|
|
"happen now")
|
619
|
3
|
if self._WARNING_ON_SAVE:
|
620
|
0
|
logger.warning("Save counter has just been increased to %d.",
|
621
|
|
self._save_counter)
|
622
|
3
|
self._save_counter += 1 # for unittest and debug purposes
|
623
|
3
|
if deadtime is None:
|
624
|
3
|
deadtime = self._loadsavedeadtime
|
625
|
|
# now write current tree structure and data to file
|
626
|
3
|
if self._lastsave + deadtime < time():
|
627
|
3
|
self._write_to_file()
|
628
|
|
else:
|
629
|
|
# make sure saving will eventually occur by launching a timer
|
630
|
3
|
if not self._savetimer.isActive():
|
631
|
3
|
self._savetimer.start()
|
632
|
|
|
633
|
3
|
@property
|
634
|
|
def _filename_stripped(self):
|
635
|
3
|
try:
|
636
|
3
|
return os.path.split(self._filename)[1].split('.')[0]
|
637
|
0
|
except:
|
638
|
0
|
return 'default'
|