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'

Read our documentation on viewing source code .

Loading