~jaap.karssenberg/zim/pyzim-gtk3

« back to all changes in this revision

Viewing changes to zim/config/basedirs.py

  • Committer: Jaap Karssenberg
  • Date: 2014-03-08 11:47:43 UTC
  • mfrom: (668.1.49 pyzim-refactor)
  • Revision ID: jaap.karssenberg@gmail.com-20140308114743-fero6uvy9zirbb4o
Merge branch with refactoring

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- coding: utf-8 -*-
2
2
 
3
 
# Copyright 2009 Jaap Karssenberg <jaap.karssenberg@gmail.com>
4
 
 
5
 
'''This module contains utilities to work with config files.
6
 
 
7
 
Main classes for storing config items are L{ConfigDictFile} which maps
8
 
"ini-style" config files, and L{ListDict} which maintains a dict of
9
 
config keys while preserving their order.
10
 
 
11
 
The search path for zim config files follows the freedesktop.org (XDG)
12
 
Base Dir specification. The functions L{config_file()} and L{data_file()}
13
 
are used to locate config and data files, while the functions
14
 
L{config_dirs()}, L{data_dir()}, and L{data_dirs()} give access to the
15
 
actual search path.
16
 
 
17
 
When this module is loaded it will check the environment parameters in
18
 
C{os.environ} and try to set proper values for C{HOME} and C{USER} if
19
 
they are not set.
 
3
# Copyright 2009-2013 Jaap Karssenberg <jaap.karssenberg@gmail.com>
 
4
 
 
5
'''This module defines the search path for zim config files following
 
6
the freedesktop.org (XDG) Base Dir specification.
20
7
'''
21
8
 
22
 
import sys
23
9
import os
24
 
import re
25
10
import logging
26
 
import types
27
 
 
28
 
if sys.version_info >= (2, 6):
29
 
        import json # in standard lib since 2.6
30
 
else:
31
 
        import simplejson as json # extra dependency
32
 
 
33
 
from zim.fs import isfile, isdir, File, Dir, FileNotFoundError, ENCODING
34
 
from zim.errors import Error
35
 
from zim.parsing import TextBuffer, split_quoted_strings
 
11
 
 
12
from zim.fs import File, Dir
 
13
from zim.environ import environ
36
14
 
37
15
 
38
16
logger = logging.getLogger('zim.config')
39
17
 
40
18
 
41
 
def get_environ(param, default=None):
42
 
        '''Get a parameter from the environment. Like C{os.environ.get()}
43
 
        but does decoding for non-ascii characters.
44
 
        @param param: the parameter to get
45
 
        @param default: the default if C{param} does not exist
46
 
        @returns: a unicode string or C{default}
47
 
        '''
48
 
        # Do NOT use zim.fs.decode here, we want real decoding on windows,
49
 
        # not just convert to unicode
50
 
        value = os.environ.get(param)
51
 
        if value is None:
52
 
                return default
53
 
        elif isinstance(value, str):
54
 
                return value.decode(ENCODING)
55
 
        else:
56
 
                return value
57
 
 
58
 
 
59
 
def get_environ_list(param, default=None, sep=None):
60
 
        '''Get a parameter from the environment and convert to a list.
61
 
        @param param: the parameter to get
62
 
        @param default: the default if C{param} does not exist
63
 
        @param sep: optional seperator, defaults to C{os.pathsep} if not given
64
 
        @returns: a list or the default
65
 
        '''
66
 
        value = get_environ(param, default)
67
 
        if isinstance(value, basestring) and value and not value.isspace():
68
 
                if sep is None:
69
 
                        sep = os.pathsep
70
 
                return value.split(sep)
71
 
        elif isinstance(value, (list, tuple)):
72
 
                return value
73
 
        else:
74
 
                return []
75
 
 
76
 
 
77
 
def set_environ(param, value):
78
 
        '''Set a parameter in the environment. Like assigning in
79
 
        C{os.environ}, but with proper encoding.
80
 
        @param param: the parameter to set
81
 
        @param value: the value, should be a string
82
 
        '''
83
 
        if isinstance(value, unicode):
84
 
                value = value.encode(ENCODING)
85
 
        os.environ[param] = value
86
 
 
87
 
 
88
 
### Inialize environment - just to be sure
89
 
 
90
 
if os.name == 'nt':
91
 
        # Windows specific environment variables
92
 
        # os.environ does not support setdefault() ...
93
 
        if not 'USER' in os.environ or not os.environ['USER']:
94
 
                os.environ['USER'] = os.environ['USERNAME']
95
 
 
96
 
        if not 'HOME' in os.environ or not os.environ['HOME']:
97
 
                if 'USERPROFILE' in os.environ:
98
 
                        os.environ['HOME'] = os.environ['USERPROFILE']
99
 
                elif 'HOMEDRIVE' in os.environ and 'HOMEPATH' in os.environ:
100
 
                        home = os.environ['HOMEDRIVE'] + os.environ['HOMEPATH']
101
 
                        os.environ['HOME'] = home
102
 
 
103
 
        if not 'APPDATA' in os.environ or not os.environ['APPDATA']:
104
 
                os.environ['APPDATA'] = os.environ['HOME'] + '\\Application Data'
105
 
 
106
 
assert isdir(get_environ('HOME')), \
107
 
        'ERROR: environment variable $HOME not set correctly'
108
 
 
109
 
if not 'USER' in os.environ or not os.environ['USER']:
110
 
        # E.g. Maemo doesn't define $USER
111
 
        os.environ['USER'] = os.path.basename(os.environ['HOME'])
112
 
        logger.info('Environment variable $USER was not set')
113
 
 
114
 
 
115
19
 
116
20
## Initialize config paths
117
21
 
122
26
XDG_CONFIG_DIRS = None #: list of L{Dir} objects for XDG config dirs path
123
27
XDG_CACHE_HOME = None #: L{Dir} for XDG cache home
124
28
 
125
 
def _set_basedirs():
 
29
def set_basedirs():
126
30
        '''This method sets the global configuration paths for according to the
127
31
        freedesktop basedir specification.
 
32
        Called automatically when module is first loaded, should be
 
33
        called explicitly only when environment has changed.
128
34
        '''
129
35
        global ZIM_DATA_DIR
130
36
        global XDG_DATA_HOME
133
39
        global XDG_CONFIG_DIRS
134
40
        global XDG_CACHE_HOME
135
41
 
136
 
        # Detect if we are running from the source dir
137
 
        try:
138
 
                if isfile('./zim.py'):
139
 
                        scriptdir = Dir('.') # maybe running module in test / debug
140
 
                else:
141
 
                        encoding = sys.getfilesystemencoding() # not 100% sure this is correct
142
 
                        path = sys.argv[0].decode(encoding)
143
 
                        scriptdir = File(path).dir
144
 
                zim_data_dir = scriptdir.subdir('data')
145
 
                if zim_data_dir.exists():
146
 
                        ZIM_DATA_DIR = zim_data_dir
147
 
                else:
148
 
                        ZIM_DATA_DIR = None
149
 
        except:
150
 
                # Catch encoding errors in argv
151
 
                logger.exception('Exception locating application data')
152
 
                ZIM_DATA_DIR = None
 
42
        # Cast string to folder
 
43
        import zim
 
44
        zim_data_dir = File(zim.ZIM_EXECUTABLE).dir.subdir('data')
 
45
        if zim_data_dir.exists():
 
46
                ZIM_DATA_DIR = zim_data_dir
153
47
 
154
48
        if os.name == 'nt':
155
 
                APPDATA = get_environ('APPDATA')
 
49
                APPDATA = environ['APPDATA']
156
50
 
157
51
                XDG_DATA_HOME = Dir(
158
 
                        get_environ('XDG_DATA_HOME', APPDATA + r'\zim\data'))
 
52
                        environ.get('XDG_DATA_HOME', APPDATA + r'\zim\data'))
159
53
 
160
54
                XDG_DATA_DIRS = map(Dir,
161
 
                        get_environ_list('XDG_DATA_DIRS', '~/.local/share/')) # Backwards compatibility
 
55
                        environ.get_list('XDG_DATA_DIRS', '~/.local/share/')) # Backwards compatibility
162
56
 
163
57
                XDG_CONFIG_HOME = Dir(
164
 
                        get_environ('XDG_CONFIG_HOME', APPDATA + r'\zim\config'))
 
58
                        environ.get('XDG_CONFIG_HOME', APPDATA + r'\zim\config'))
165
59
 
166
60
                XDG_CONFIG_DIRS = map(Dir,
167
 
                        get_environ_list('XDG_CONFIG_DIRS', '~/.config/')) # Backwards compatibility
 
61
                        environ.get_list('XDG_CONFIG_DIRS', '~/.config/')) # Backwards compatibility
168
62
 
169
63
                try:
170
64
                        import _winreg as wreg
171
65
                        wreg_key = wreg.OpenKey(
172
66
                                wreg.HKEY_CURRENT_USER,
173
67
                                r'Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders')
174
 
                        cache_dir = str(wreg.QueryValueEx(wreg_key, "Cache")[0].replace(u'%USERPROFILE%', get_environ['USERPROFILE']))
 
68
                        cache_dir = str(wreg.QueryValueEx(wreg_key, "Cache")[0].replace(u'%USERPROFILE%', environ['USERPROFILE']))
175
69
                        wreg.CloseKey(wreg_key)
176
70
                except:
177
 
                        cache_dir = os.environ['TEMP']
 
71
                        cache_dir = environ['TEMP']
178
72
 
179
73
                XDG_CACHE_HOME = Dir(
180
 
                        get_environ('XDG_CACHE_HOME', cache_dir + r'\zim'))
 
74
                        environ.get('XDG_CACHE_HOME', cache_dir + r'\zim'))
181
75
        else:
182
76
                XDG_DATA_HOME = Dir(
183
 
                        get_environ('XDG_DATA_HOME', '~/.local/share/'))
 
77
                        environ.get('XDG_DATA_HOME', '~/.local/share/'))
184
78
 
185
79
                XDG_DATA_DIRS = map(Dir,
186
 
                        get_environ_list('XDG_DATA_DIRS', ('/usr/share/', '/usr/local/share/')))
 
80
                        environ.get_list('XDG_DATA_DIRS', ('/usr/share/', '/usr/local/share/')))
187
81
 
188
82
                XDG_CONFIG_HOME = Dir(
189
 
                        get_environ('XDG_CONFIG_HOME', '~/.config/'))
 
83
                        environ.get('XDG_CONFIG_HOME', '~/.config/'))
190
84
 
191
85
                XDG_CONFIG_DIRS = map(Dir,
192
 
                        get_environ_list('XDG_CONFIG_DIRS', ('/etc/xdg/',)))
 
86
                        environ.get_list('XDG_CONFIG_DIRS', ('/etc/xdg/',)))
193
87
 
194
88
                XDG_CACHE_HOME = Dir(
195
 
                        get_environ('XDG_CACHE_HOME', '~/.cache'))
 
89
                        environ.get('XDG_CACHE_HOME', '~/.cache'))
196
90
 
197
91
 
198
92
# Call on module initialization to set defaults
199
 
_set_basedirs()
 
93
set_basedirs()
200
94
 
201
95
 
202
96
def log_basedirs():
213
107
        logger.debug('Set XDG_CONFIG_DIRS to %s', XDG_CONFIG_DIRS)
214
108
        logger.debug('Set XDG_CACHE_HOME to %s', XDG_CACHE_HOME)
215
109
 
216
 
 
217
 
def data_dirs(path=None):
218
 
        '''Generator listing paths that contain zim data files in the order
219
 
        that they should be searched. These will be the equivalent of
220
 
        e.g. "~/.local/share/zim", "/usr/share/zim", etc.
221
 
        @param path: a file path relative to to the data dir, including this
222
 
        will list sub-folders with this relative path.
223
 
        @returns: yields L{Dir} objects for the data dirs
224
 
        '''
225
 
        zimpath = ['zim']
226
 
        if path:
227
 
                if isinstance(path, basestring):
228
 
                        path = [path]
229
 
                assert not path[0] == 'zim'
230
 
                zimpath.extend(path)
231
 
 
232
 
        yield XDG_DATA_HOME.subdir(zimpath)
233
 
 
234
 
        if ZIM_DATA_DIR:
235
 
                if path:
236
 
                        yield ZIM_DATA_DIR.subdir(path)
237
 
                else:
238
 
                        yield ZIM_DATA_DIR
239
 
 
240
 
        for dir in XDG_DATA_DIRS:
241
 
                yield dir.subdir(zimpath)
242
 
 
243
 
def data_dir(path):
244
 
        '''Get an data dir sub-folder.  Will look up C{path} relative
245
 
        to all data dirs and return the first one that exists. Use this
246
 
        function to find any folders from the "data/" folder in the source
247
 
        package.
248
 
        @param path:  a file path relative to to the data dir
249
 
        @returns: a L{Dir} object or C{None}
250
 
        '''
251
 
        for dir in data_dirs(path):
252
 
                if dir.exists():
253
 
                        return dir
254
 
        else:
255
 
                return None
256
 
 
257
 
def data_file(path):
258
 
        '''Get a data file. Will look up C{path} relative to all data dirs
259
 
        and return the first one that exists. Use this function to find
260
 
        any files from the "data/" folder in the source package.
261
 
        @param path:  a file path relative to to the data dir (e.g. "zim.png")
262
 
        @returns: a L{File} object or C{None}
263
 
        '''
264
 
        for dir in data_dirs():
265
 
                file = dir.file(path)
266
 
                if file.exists():
267
 
                        return file
268
 
        else:
269
 
                return None
270
 
 
271
 
def config_dirs():
272
 
        '''Generator listing paths for zim config files. These will be the
273
 
        equivalent of e.g. "~/.config/zim", "/etc/xdg/zim" etc.
274
 
 
275
 
        Zim is not strictly XDG conformant by installing default config
276
 
        files in "/usr/share/zim" instead of in "/etc/xdg/zim". Therefore
277
 
        this function yields both.
278
 
 
279
 
        @returns: yields L{Dir} objects for all config and data dirs
280
 
        '''
281
 
        yield XDG_CONFIG_HOME.subdir(('zim'))
282
 
        for dir in XDG_CONFIG_DIRS:
283
 
                yield dir.subdir(('zim'))
284
 
        for dir in data_dirs():
285
 
                yield dir
286
 
 
287
 
 
288
 
def config_file(path):
289
 
        '''Alias for constructing a L{ConfigFile} object
290
 
        @param path: either basename as string or tuple with relative path
291
 
        @returns: a L{ConfigFile}
292
 
        '''
293
 
        return ConfigFile(path)
294
 
 
295
 
 
296
 
def get_config(path):
297
 
        '''Convenience method to construct a L{ConfigDictFile} based on a
298
 
        C{ConfigFile}.
299
 
        @param path: either basename as string or tuple with relative path
300
 
        @returns: a L{ConfigDictFile}
301
 
        '''
302
 
        file = ConfigFile(path)
303
 
        return ConfigDictFile(file)
304
 
 
305
 
 
306
 
def list_profiles():
307
 
        '''Returns a list known preferences profiles.'''
308
 
        profiles = []
309
 
        for dir in config_dirs():
310
 
                for f in dir.subdir('profiles').list():
311
 
                        if f.endswith('.conf'):
312
 
                                profiles.append(f[:-5])
313
 
        profiles.sort()
314
 
        return profiles
315
 
 
316
 
 
317
 
def user_dirs():
318
 
        '''Get the XDG user dirs.
319
 
        @returns: a dict with directories for the XDG user dirs. These are
320
 
        typically defined in "~/.config/user-dirs.dirs". Common user dirs
321
 
        are: "XDG_DESKTOP_DIR", "XDG_DOWNLOAD_DIR", etc. If no definition
322
 
        is found an empty dict will be returned.
323
 
        '''
324
 
        dirs = {}
325
 
        file = XDG_CONFIG_HOME.file('user-dirs.dirs')
326
 
        try:
327
 
                for line in file.readlines():
328
 
                        line = line.strip()
329
 
                        if line.isspace() or line.startswith('#'):
330
 
                                continue
331
 
                        else:
332
 
                                try:
333
 
                                        assert '=' in line
334
 
                                        key, value = line.split('=', 1)
335
 
                                        value = os.path.expandvars(value.strip('"'))
336
 
                                        dirs[key] = Dir(value)
337
 
                                except:
338
 
                                        logger.exception('Exception while parsing %s', file)
339
 
        except FileNotFoundError:
340
 
                pass
341
 
        return dirs
342
 
 
343
 
 
344
 
class ConfigFile(object):
345
 
        '''Container object for a config file
346
 
 
347
 
        Maps to a "base" file in the home folder, used to write new values,
348
 
        and one or more default files, e.g. in C{/usr/share/zim}, which
349
 
        are the fallback to get default values
350
 
 
351
 
        @ivar file: the underlying file object for the base config file
352
 
        in the home folder
353
 
 
354
 
        @note: this class implement similar API to the L{File} class but
355
 
        is explicitly not a sub-class of L{File} because config files should
356
 
        typically not be moved, renamed, etc. It just implements the reading
357
 
        and writing methods.
358
 
        '''
359
 
 
360
 
        def __init__(self, path, file=None):
361
 
                '''Constructor
362
 
                @param path: either basename as string or tuple with relative path,
363
 
                is resolved relative to the default config dir for zim.
364
 
                @param file: optional argument for some special case to
365
 
                override the base file in the home folder.
366
 
                '''
367
 
                if isinstance(path, basestring):
368
 
                        path = (path,)
369
 
                self._path = tuple(path)
370
 
                if file:
371
 
                        self.file = file
372
 
                else:
373
 
                        self.file = File((XDG_CONFIG_HOME, 'zim') + self._path)
374
 
 
375
 
        def __repr__(self):
376
 
                return '<%s: %s>' % (self.__class__.__name__, self.file.path)
377
 
 
378
 
        def __eq__(self, other):
379
 
                return isinstance(other, ConfigFile) \
380
 
                and other._path == self._path \
381
 
                and other.file == self.file
382
 
 
383
 
        @property
384
 
        def basename(self):
385
 
                return self.file.basename
386
 
 
387
 
        def default_files(self):
388
 
                '''Generator that yields default config files (read-only) to
389
 
                use instead of the standard file when it is still empty.
390
 
                Typically only the first one is used.
391
 
                '''
392
 
                for dir in config_dirs():
393
 
                        default = dir.file(self._path)
394
 
                        if default.exists():
395
 
                                yield default
396
 
 
397
 
        def touch(self):
398
 
                '''Ensure the custom file in the home folder exists. Either by
399
 
                copying a default config file, or touching an empty file.
400
 
                Intended to be called before trying to edit the file with an
401
 
                external editor.
402
 
                '''
403
 
                if not self.file.exists():
404
 
                        for file in self.default_files():
405
 
                                file.copyto(self.file)
406
 
                                break
407
 
                        else:
408
 
                                self.file.touch() # create empty file
409
 
 
410
 
        def read(self, fail=False):
411
 
                '''Read the base file or first default file
412
 
                @param fail: if C{True} a L{FileNotFoundError} error is raised
413
 
                when neither the base file or a default file are found. If
414
 
                C{False} it will return C{''} for a non-existing file.
415
 
                @returns: file content as a string
416
 
                '''
417
 
                try:
418
 
                        return self.file.read()
419
 
                except FileNotFoundError:
420
 
                        for file in self.default_files():
421
 
                                return file.read()
422
 
                        else:
423
 
                                if fail:
424
 
                                        raise
425
 
                                else:
426
 
                                        return ''
427
 
 
428
 
        def readlines(self, fail=False):
429
 
                '''Read the base file or first default file
430
 
                @param fail: if C{True} a L{FileNotFoundError} error is raised
431
 
                when neither the base file or a default file are found. If
432
 
                C{False} it will return C{[]} for a non-existing file.
433
 
                @returns: file content as a list of lines
434
 
                '''
435
 
                try:
436
 
                        return self.file.readlines()
437
 
                except FileNotFoundError:
438
 
                        for file in self.default_files():
439
 
                                return file.readlines()
440
 
                        else:
441
 
                                if fail:
442
 
                                        raise
443
 
                                else:
444
 
                                        return []
445
 
 
446
 
        # Not implemented: read_async and readlines_async
447
 
 
448
 
        def write(self, text):
449
 
                '''Write base file, see L{File.write()}'''
450
 
                self.file.write(text)
451
 
 
452
 
        def writelines(self, lines):
453
 
                '''Write base file, see L{File.writelines()}'''
454
 
                self.file.writelines(lines)
455
 
 
456
 
        def write_async(self, text, callback=None, data=None):
457
 
                '''Write base file async, see L{File.write_async()}'''
458
 
                return self.file.write_async(text, callback=callback, data=data)
459
 
 
460
 
        def writelines_async(self, lines, callback=None, data=None):
461
 
                '''Write base file async, see L{File.writelines_async()}'''
462
 
                return self.file.writelines_async(lines, callback=callback, data=data)
463
 
 
464
 
        def remove(self):
465
 
                '''Remove user file, leaves default files in place'''
466
 
                if self.file.exists():
467
 
                        return self.file.remove()
468
 
 
469
 
 
470
 
def check_class_allow_empty(value, default):
471
 
        '''Check function for L{ListDict.setdefault()} which ensures the
472
 
        value is of the same class as the default if it is set, but also
473
 
        allows it to be empty (empty string or C{None}). This is
474
 
        the same as the default behavior when "C{allow_empty}" is C{True}.
475
 
        It will convert C{list} type to C{tuple} automatically if the
476
 
        default is a tuple.
477
 
 
478
 
        This function can be used in cases where the check is provided
479
 
        inderictly and C{allow_empty} can not be passed along, e.g.
480
 
        in the definition of plugin preferences.
481
 
 
482
 
        @param value: the value in the dict
483
 
        @param default: the default that is set
484
 
        @returns: the new value to set
485
 
        @raises AssertionError: when the value if of the wrong class
486
 
        (which will result in C{setdefault()} setting the default value)
487
 
        '''
488
 
        klass = default.__class__
489
 
        if issubclass(klass, basestring):
490
 
                klass = basestring
491
 
 
492
 
        if value in ('', None) or isinstance(value, klass):
493
 
                return value
494
 
        elif klass is tuple and isinstance(value, list):
495
 
                # Special case because json does not know difference list or tuple
496
 
                return tuple(value)
497
 
        else:
498
 
                raise AssertionError, 'should be of type: %s' % klass
499
 
 
500
 
 
501
 
def value_is_coord(value, default):
502
 
        '''Check function for L{ListDict.setdefault()} which will check
503
 
        whether the value is a coordinate (a tuple of two integers). This
504
 
        is e.g. used to store for window coordinates. If the value is a
505
 
        list of two integers, it will automatically be converted to a tuple.
506
 
 
507
 
        @param value: the value in the dict
508
 
        @param default: the default that is set
509
 
        @returns: the new value to set
510
 
        @raises AssertionError: when the value is not a coordinate tuple
511
 
        (which will result in C{setdefault()} setting the default value)
512
 
        '''
513
 
        if isinstance(value, list):
514
 
                value = tuple(value)
515
 
 
516
 
        if (
517
 
                isinstance(value, tuple)
518
 
                and len(value) == 2
519
 
                and isinstance(value[0], int)
520
 
                and isinstance(value[1], int)
521
 
        ):
522
 
                return value
523
 
        else:
524
 
                raise AssertionError, 'should be coordinate (tuple of int)'
525
 
 
526
 
 
527
 
class ListDict(dict):
528
 
        '''Class that behaves like a dict but keeps items in same order.
529
 
        This is the base class for all dicts holding config items in zim.
530
 
        Most importantly it is used for each section in the L{ConfigDict}.
531
 
        Because it remembers the order of the items in the dict, the order
532
 
        in which they will be written to a config file is predictable.
533
 
        Another important function is to check the config values have
534
 
        proper values, this is enforced by L{setdefault()}.
535
 
 
536
 
        @ivar modified: C{True} when the values were modified, used to e.g.
537
 
        track when a config needs to be written back to file
538
 
        '''
539
 
 
540
 
        def __init__(self):
541
 
                self.order = []
542
 
                self._modified = False
543
 
 
544
 
        def __repr__(self):
545
 
                return '<%s: %s>' % (self.__class__.__name__, dict.__repr__(self))
546
 
 
547
 
        def copy(self):
548
 
                '''Shallow copy of the items
549
 
                @returns: a new object of the same class with the same items
550
 
                '''
551
 
                new = self.__class__()
552
 
                new.update(self)
553
 
                return new
554
 
 
555
 
        @property
556
 
        def modified(self):
557
 
                if self._modified:
558
 
                        return True
559
 
                else:
560
 
                        return any(v.modified for v in self.values()
561
 
                                                                        if isinstance(v, ListDict))
562
 
 
563
 
        def set_modified(self, modified):
564
 
                '''Set the modified state. Used to reset modified to C{False}
565
 
                after the configuration has been saved to file.
566
 
                @param modified: C{True} or C{False}
567
 
                '''
568
 
                if modified:
569
 
                        self._modified = True
570
 
                else:
571
 
                        self._modified = False
572
 
                        for v in self.values():
573
 
                                if isinstance(v, ListDict):
574
 
                                        v.set_modified(False)
575
 
 
576
 
        def update(D, E=None, **F):
577
 
                '''Like C{dict.update()}'''
578
 
                if E and hasattr(E, 'keys'):
579
 
                        for k in E: D[k] = E[k]
580
 
                elif E:
581
 
                        for (k, v) in E: D[k] = v
582
 
                for k in F: D[k] = F[k]
583
 
 
584
 
        def __setitem__(self, k, v):
585
 
                dict.__setitem__(self, k, v)
586
 
                self._modified = True
587
 
                if not k in self.order:
588
 
                        self.order.append(k)
589
 
 
590
 
        def __delitem__(self, k):
591
 
                dict.__delitem__(self, k)
592
 
                self.order.remove(k)
593
 
 
594
 
        def __iter__(self):
595
 
                return iter(self.order)
596
 
 
597
 
        def pop(self, k):
598
 
                '''Like C{dict.pop()}'''
599
 
                v = dict.pop(self, k)
600
 
                self.order.remove(k)
601
 
                return v
602
 
 
603
 
        def setdefault(self, key, default, check=None, allow_empty=False):
604
 
                '''Set the default value for a configuration item.
605
 
 
606
 
                Compatible with C{dict.setdefault()} but extended with
607
 
                functionality to check the value that is in the dict, and use
608
 
                the default if the value is mal-formed. This is used extensively
609
 
                in zim to do a sanity check on values in the configuration
610
 
                files. If you initialize the config items with this method you
611
 
                can assume them to be safe afterward and avoid a lot of checks
612
 
                or bugs later in the code.
613
 
 
614
 
                @param key: the dict key
615
 
                @param default: the default value for this key
616
 
 
617
 
                @param check: the check to do on the values, when the check
618
 
                fails the value is considered mal-formed and the default is
619
 
                used while a warning is logged.
620
 
 
621
 
                If C{check} is C{None} the default behavior will be to compare
622
 
                the classes of the set value and the default and enforce them to
623
 
                be of the same type. Automatic conversion is done for values of
624
 
                type C{list} with defaults of type C{tuple}. And for defaults of
625
 
                type C{str} or C{unicode} the C{basestring} type is used as
626
 
                check. As a special case when the default is C{None} the check
627
 
                is not allowed to be C{None} as well.
628
 
 
629
 
                If C{check} is given and it is a class the existing value will be
630
 
                checked to be of that class. Same special case for tuples
631
 
                and strings applies here.
632
 
 
633
 
                If C{check} is given and is a C{set}, C{list} or C{tuple} the
634
 
                value will be tested to be in this set or list.
635
 
 
636
 
                If the default is an integer and C{check} is a tuple of two
637
 
                integers, the check will be that the value is in this range.
638
 
                (For compatibility with L{InputForm} extra argument for integer
639
 
                spin boxes.)
640
 
 
641
 
                If C{check} is given and it is a function it will be used to
642
 
                check the value in the dictionary if it exists. The function
643
 
                is called as::
644
 
 
645
 
                        check(value, default)
646
 
 
647
 
                Where C{value} is the current value in the dict and C{default}
648
 
                is the default value that was provided. The function can not
649
 
                only check the value, it can also do on the fly modifications,
650
 
                e.g. to coerce it into a specific type. If the value is OK the
651
 
                function should return the (modified) value, if not it should
652
 
                raise an C{AssertionError}. When this error is raised the
653
 
                default is used and the dict is considered being modified.
654
 
 
655
 
                ( Note that 'assert' statements in the code can be removed
656
 
                by code optimization, so explicitly call "C{raise AssertionError}". )
657
 
 
658
 
                Examples of functions that can be used as a check are:
659
 
                L{check_class_allow_empty} and L{value_is_coord}.
660
 
 
661
 
                @param allow_empty: if C{True} the value is allowed to be empty
662
 
                (either empty string or C{None}). In this case the default is
663
 
                not set to overwrite an empty value, but only for a mal-formed
664
 
                value or for a value that doesn't exist yet in the dict.
665
 
                '''
666
 
                assert not (default is None and check is None), \
667
 
                        'Bad practice to set default to None without check'
668
 
 
669
 
                if not key in self:
670
 
                        self.__setitem__(key, default)
671
 
                        return self[key]
672
 
 
673
 
                if check is None:
674
 
                        klass = default.__class__
675
 
                        if issubclass(klass, basestring):
676
 
                                klass = basestring
677
 
                        check = klass
678
 
 
679
 
                if default in ('', None):
680
 
                        allow_empty = True
681
 
 
682
 
                if allow_empty and self[key] in ('', None):
683
 
                        return self[key]
684
 
 
685
 
                if isinstance(check, (type, types.ClassType)): # is a class
686
 
                        klass = check
687
 
                        if not (allow_empty and default in ('', None)):
688
 
                                assert isinstance(default, klass), 'Default does not have correct class'
689
 
 
690
 
                        if not isinstance(self[key], klass):
691
 
                                if klass is tuple and isinstance(self[key], list):
692
 
                                        # Special case because json does not know difference list or tuple
693
 
                                        modified = self.modified
694
 
                                        self.__setitem__(key, tuple(self[key]))
695
 
                                        self.set_modified(modified) # don't change modified state
696
 
                                elif hasattr(klass, 'new_from_zim_config'):
697
 
                                        # Class has special contructor
698
 
                                        modified = self.modified
699
 
                                        try:
700
 
                                                self.__setitem__(key, klass.new_from_zim_config(self[key]))
701
 
                                        except:
702
 
                                                logger.exception(
703
 
                                                        'Invalid config value for %s: "%s"',
704
 
                                                        key, self[key])
705
 
                                        self.set_modified(modified) # don't change modified state
706
 
                                else:
707
 
                                        logger.warn(
708
 
                                                'Invalid config value for %s: "%s" - should be of type %s',
709
 
                                                key, self[key], klass)
710
 
                                        self.__setitem__(key, default)
711
 
                        elif self[key] == '':
712
 
                                        # Special case for empty string
713
 
                                        logger.warn(
714
 
                                                'Invalid config value for %s: "%s" - not allowed to be empty',
715
 
                                                key, self[key])
716
 
                                        self.__setitem__(key, default)
717
 
                        else:
718
 
                                pass # value is OK
719
 
                elif isinstance(check, (set, list)) \
720
 
                or (isinstance(check, tuple) and not isinstance(default, int)):
721
 
                        if not (allow_empty and default in ('', None)):
722
 
                                # HACK to allow for preferences with "choice" item that has
723
 
                                # a list of tuples as argumnet
724
 
                                if all(isinstance(t, tuple) for t in check):
725
 
                                        check = list(check) # copy
726
 
                                        check += [t[0] for t in check]
727
 
                                assert default in check, 'Default is not within allowed set'
728
 
 
729
 
                        # HACK to allow the value to be a tuple...
730
 
                        if all(isinstance(t, tuple) for t in check) \
731
 
                        and isinstance(self[key], list):
732
 
                                modified = self.modified
733
 
                                self.__setitem__(key, tuple(self[key]))
734
 
                                self.set_modified(modified)
735
 
 
736
 
                        if not self[key] in check:
737
 
                                logger.warn(
738
 
                                                'Invalid config value for %s: "%s" - should be one of %s',
739
 
                                                key, self[key], unicode(check))
740
 
                                self.__setitem__(key, default)
741
 
                        else:
742
 
                                pass # value is OK
743
 
                elif isinstance(check, tuple) and isinstance(default, int):
744
 
                        assert len(check) == 2 \
745
 
                                and isinstance(check[0], int) \
746
 
                                and isinstance(check[1], int)
747
 
                        if not isinstance(self[key], int):
748
 
                                logger.warn(
749
 
                                        'Invalid config value for %s: "%s" - should be integer',
750
 
                                        key, self[key])
751
 
                                self.__setitem__(key, default)
752
 
                        elif not check[0] <= self[key] <= check[1]:
753
 
                                logger.warn(
754
 
                                        'Invalid config value for %s: "%s" - should be between %i and %i',
755
 
                                        key, self[key], check[0], check[1])
756
 
                                self.__setitem__(key, default)
757
 
                        else:
758
 
                                pass # value is OK
759
 
                else: # assume callable
760
 
                        modified = self.modified
761
 
                        try:
762
 
                                v = check(self[key], default)
763
 
                                self.__setitem__(key, v)
764
 
                                self.set_modified(modified)
765
 
                        except AssertionError, error:
766
 
                                logger.warn(
767
 
                                        'Invalid config value for %s: "%s" - %s',
768
 
                                        key, self[key], error.args[0])
769
 
                                self.__setitem__(key, default)
770
 
 
771
 
                return self[key]
772
 
 
773
 
        def keys(self):
774
 
                '''Like C{dict.keys()}'''
775
 
                return self.order[:]
776
 
 
777
 
        def items(self):
778
 
                '''Like C{dict.items()}'''
779
 
                return tuple(map(lambda k: (k, self[k]), self.order))
780
 
 
781
 
        def set_order(self, order):
782
 
                '''Change the order in which items are listed.
783
 
 
784
 
                @param order: a list of keys in a specific order. Items in the
785
 
                dict that do not appear in the list will be moved to the end.
786
 
                Items in the list that are not in the dict are ignored.
787
 
                '''
788
 
                order = list(order[:]) # copy and convert
789
 
                oldorder = set(self.order)
790
 
                neworder = set(order)
791
 
                for k in neworder - oldorder: # keys not in the dict
792
 
                        order.remove(k)
793
 
                for k in oldorder - neworder: # keys not in the list
794
 
                        order.append(k)
795
 
                neworder = set(order)
796
 
                assert neworder == oldorder
797
 
                self.order = order
798
 
 
799
 
 
800
 
class ConfigDict(ListDict):
801
 
        '''Dict to represent a configuration file in "ini-style". Since the
802
 
        ini-file is devided in section this is represented as a dict of
803
 
        dicts. This class represents the top-level with a key for each
804
 
        section. The values are in turn L{ListDict}s which contain the
805
 
        key value pairs in that section.
806
 
 
807
 
        A typical file might look like::
808
 
 
809
 
          [Section1]
810
 
          param1=foo
811
 
          param2=bar
812
 
 
813
 
          [Section2]
814
 
          enabled=True
815
 
          data={'foo': 1, 'bar': 2}
816
 
 
817
 
        values can either be simple string, number, or one of "True",
818
 
        "False" and "None", or a complex data structure encoded with the
819
 
        C{json} module.
820
 
 
821
 
        Sections are auto-vivicated when a non-existing item is retrieved.
822
 
 
823
 
        By default when parsing sections of the same name they will be
824
 
        merged and values that appear under the same section name later in
825
 
        the file will overwrite values that appeared earlier. As a special
826
 
        case we can support sections that repeat under the same section name.
827
 
        To do this assign the section name a list before parsing.
828
 
 
829
 
        Sections and parameters whose name start with '_' are considered as
830
 
        private and are not stored when the config is written to file. This
831
 
        can be used for caching values that should not be persistent across
832
 
        instances.
833
 
        '''
834
 
 
835
 
        def __getitem__(self, k):
836
 
                if not k in self:
837
 
                        self[k] = ListDict()
838
 
                return dict.__getitem__(self, k)
839
 
 
840
 
        def parse(self, text):
841
 
                '''Parse an "ini-style" configuration. Fills the dictionary
842
 
                with values from this text, wil merge with existing sections and
843
 
                overwrite existing values.
844
 
                @param text: a string or a list of lines
845
 
                '''
846
 
                # Note that we explicitly do _not_ support comments on the end
847
 
                # of a line. This is because "#" could be a valid character in
848
 
                # a config value.
849
 
                if isinstance(text, basestring):
850
 
                        text = text.splitlines(True)
851
 
                section = None
852
 
                for line in text:
853
 
                        line = line.strip()
854
 
                        if not line or line.startswith('#'):
855
 
                                continue
856
 
                        elif line.startswith('[') and line.endswith(']'):
857
 
                                name = line[1:-1].strip()
858
 
                                section = self[name]
859
 
                                if isinstance(section, list):
860
 
                                        section.append(ListDict())
861
 
                                        section = section[-1]
862
 
                        elif '=' in line:
863
 
                                parameter, rawvalue = line.split('=', 1)
864
 
                                parameter = str(parameter.rstrip()) # no unicode
865
 
                                rawvalue = rawvalue.lstrip()
866
 
                                try:
867
 
                                        value = self._decode_value(parameter, rawvalue)
868
 
                                        section[parameter] = value
869
 
                                except:
870
 
                                        logger.warn('Failed to parse value for key "%s": %s', parameter, rawvalue)
871
 
                        else:
872
 
                                logger.warn('Could not parse line: %s', line)
873
 
 
874
 
        # Separated out as this will be slightly different for .desktop files
875
 
        # we ignore the key - but DesktopEntryDict uses them
876
 
        def _decode_value(self, key, value):
877
 
                if len(value) == 0:
878
 
                        return ''
879
 
                if value == 'True': return True
880
 
                elif value == 'False': return False
881
 
                elif value == 'None': return None
882
 
                elif value[0] in ('{', '['):
883
 
                        return json.loads(value)
884
 
                else:
885
 
                        try:
886
 
                                value = int(value)
887
 
                                return value
888
 
                        except: pass
889
 
 
890
 
                        try:
891
 
                                value = float(value)
892
 
                                return value
893
 
                        except: pass
894
 
 
895
 
                        return json.loads('"%s"' % value.replace('"', r'\"')) # force string
896
 
 
897
 
        def dump(self):
898
 
                '''Serialize the config to a "ini-style" config file.
899
 
                @returns: a list of lines with text in "ini-style" formatting
900
 
                '''
901
 
                lines = []
902
 
                def dump_section(name, parameters):
903
 
                        try:
904
 
                                lines.append('[%s]\n' % section)
905
 
                                for param, value in parameters.items():
906
 
                                        if not param.startswith('_'):
907
 
                                                lines.append('%s=%s\n' % (param, self._encode_value(value)))
908
 
                                lines.append('\n')
909
 
                        except:
910
 
                                logger.exception('Dumping section [%s] failed:\n%r', name, parameters)
911
 
 
912
 
                for section, parameters in self.items():
913
 
                        if parameters and not section.startswith('_'):
914
 
                                if isinstance(parameters, list):
915
 
                                        for param in parameters:
916
 
                                                dump_section(section, param)
917
 
                                else:
918
 
                                        dump_section(section, parameters)
919
 
 
920
 
                return lines
921
 
 
922
 
        def _encode_value(self, value):
923
 
                if isinstance(value, basestring):
924
 
                        return json.dumps(value)[1:-1] # get rid of quotes
925
 
                elif value is True: return 'True'
926
 
                elif value is False: return 'False'
927
 
                elif value is None: return 'None'
928
 
                elif hasattr(value, 'serialize_zim_config'):
929
 
                        return value.serialize_zim_config()
930
 
                else:
931
 
                        return json.dumps(value, separators=(',',':'))
932
 
                                # specify separators for compact encoding
933
 
 
934
 
 
935
 
class ConfigFileMixin(ListDict):
936
 
        '''Mixin class for reading and writing config to file, can be used
937
 
        with any parent class that has a C{parse()}, a C{dump()}, and a
938
 
        C{set_modified()} method. See L{ConfigDict} for the documentation
939
 
        of these methods.
940
 
        '''
941
 
 
942
 
        def __init__(self, file):
943
 
                '''Constructor
944
 
                @param file: a L{File} or L{ConfigFile} object for reading and
945
 
                writing the config.
946
 
                '''
947
 
                ListDict.__init__(self)
948
 
                self.file = file
949
 
                try:
950
 
                        self.read()
951
 
                        self.set_modified(False)
952
 
                except FileNotFoundError:
953
 
                        pass
954
 
 
955
 
        def read(self):
956
 
                '''Read data from file'''
957
 
                # No flush here - this is used by change_file()
958
 
                # but may change in the future - so do not depend on it
959
 
                logger.debug('Loading config from: %s', self.file)
960
 
                self.parse(self.file.readlines())
961
 
                # Will fail with FileNotFoundError if file does not exist
962
 
 
963
 
        def write(self):
964
 
                '''Write data and set C{modified} to C{False}'''
965
 
                self.file.writelines(self.dump())
966
 
                self.set_modified(False)
967
 
 
968
 
        def write_async(self):
969
 
                '''Write data asynchronously and set C{modified} to C{False}
970
 
                @returns: an L{AsyncOperation} object
971
 
                '''
972
 
                operation = self.file.writelines_async(self.dump())
973
 
                # TODO do we need async error handling here ?
974
 
                self.set_modified(False)
975
 
                return operation
976
 
 
977
 
        def change_file(self, file, merge=True):
978
 
                '''Change the underlaying file used to read/write data
979
 
                Used to switch to a new config file without breaking existing
980
 
                references to config sections.
981
 
                @param file: a L{File} or L{ConfigFile} object for the new config
982
 
                @param merge: if C{True} the new file will be read (if it exists)
983
 
                and values in this dict will be updated.
984
 
                '''
985
 
                self.file = file
986
 
                try:
987
 
                        self.read()
988
 
                        self.set_modified(True)
989
 
                                # This is the correct state because after reading we are
990
 
                                # merged state, so does not matching file content
991
 
                except FileNotFoundError:
992
 
                        pass
993
 
 
994
 
 
995
 
class ConfigDictFile(ConfigFileMixin, ConfigDict):
996
 
        pass
997
 
 
998
 
 
999
 
class HeaderParsingError(Error):
1000
 
        '''Error when parsing a L{HeadersDict}'''
1001
 
 
1002
 
        description = '''\
1003
 
Invalid data was found in a block with headers.
1004
 
This probably means the header block was corrupted
1005
 
and can not be read correctly.'''
1006
 
 
1007
 
        def __init__(self, line):
1008
 
                self.msg = 'Invalid header >>%s<<' % line.strip('\n')
1009
 
 
1010
 
 
1011
 
class HeadersDict(ListDict):
1012
 
        '''This class maps a set of headers in the rfc822 format.
1013
 
        Can e.g. look like::
1014
 
 
1015
 
                Content-Type: text/x-zim-wiki
1016
 
                Wiki-Format: zim 0.4
1017
 
                Creation-Date: 2010-12-14T14:15:09.134955
1018
 
 
1019
 
        Header names are always kept in "title()" format to ensure
1020
 
        case-insensitivity.
1021
 
        '''
1022
 
 
1023
 
        _is_header_re = re.compile('^([\w\-]+):\s+(.*)')
1024
 
        _is_continue_re = re.compile('^(\s+)(?=\S)')
1025
 
 
1026
 
        def __init__(self, text=None):
1027
 
                '''Constructor
1028
 
 
1029
 
                @param text: the header text, passed on to L{parse()}
1030
 
                '''
1031
 
                ListDict.__init__(self)
1032
 
                if not text is None:
1033
 
                        self.parse(text)
1034
 
 
1035
 
        def __getitem__(self, k):
1036
 
                return ListDict.__getitem__(self, k.title())
1037
 
 
1038
 
        def __setitem__(self, k, v):
1039
 
                return ListDict.__setitem__(self, k.title(), v)
1040
 
 
1041
 
        def read(self, lines):
1042
 
                '''Checks for headers at the start of the list of lines and
1043
 
                read them into the dict until the first empty line. Will remove
1044
 
                any lines belonging to the header block from the original list,
1045
 
                so after this method returns the input does no longer contain
1046
 
                the header block.
1047
 
                @param lines: a list of lines
1048
 
                '''
1049
 
                self._parse(lines, fatal=False)
1050
 
                if lines and lines[0].isspace():
1051
 
                        lines.pop(0)
1052
 
 
1053
 
        def parse(self, text):
1054
 
                '''Adds headers defined in 'text' to the dict.
1055
 
                Trailing whitespace is ignored.
1056
 
 
1057
 
                @param text: a header block, either as string or as a list of lines.
1058
 
 
1059
 
                @raises HeaderParsingError: when C{text} is not a valid header
1060
 
                block
1061
 
                '''
1062
 
                if isinstance(text, basestring):
1063
 
                        lines = text.rstrip().splitlines(True)
1064
 
                else:
1065
 
                        lines = text[:] # make copy so we do not destry the original
1066
 
                self._parse(lines)
1067
 
 
1068
 
        def _parse(self, lines, fatal=True):
1069
 
                header = None
1070
 
                while lines:
1071
 
                        is_header = self._is_header_re.match(lines[0])
1072
 
                        if is_header:
1073
 
                                header = is_header.group(1)
1074
 
                                value  = is_header.group(2)
1075
 
                                self[header] = value.strip()
1076
 
                        elif self._is_continue_re.match(lines[0]) and not header is None:
1077
 
                                self[header] += '\n' + lines[0].strip()
1078
 
                        else:
1079
 
                                if fatal:
1080
 
                                        raise HeaderParsingError, lines[0]
1081
 
                                else:
1082
 
                                        break
1083
 
                        lines.pop(0)
1084
 
 
1085
 
        def dump(self, strict=False):
1086
 
                '''Serialize the dict to a header block in rfc822 header format.
1087
 
 
1088
 
                @param strict: if C{True} lines will be properly terminated
1089
 
                with '\\r\\n' instead of '\\n'.
1090
 
 
1091
 
                @returns: the header block as a list of lines
1092
 
                '''
1093
 
                buffer = []
1094
 
                for k, v in self.items():
1095
 
                        v = v.strip().replace('\n', '\n\t')
1096
 
                        buffer.extend((k, ': ', v, '\n'))
1097
 
                text = ''.join(buffer)
1098
 
 
1099
 
                if strict:
1100
 
                        text = text.replace('\n', '\r\n')
1101
 
 
1102
 
                return text.splitlines(True)
1103
 
 
1104
 
 
1105
 
class HierarchicDict(object):
1106
 
        '''This class implements a data store that behaves as a hierarchig
1107
 
        dict of dicts. Each key in this object is considered a hierarchic
1108
 
        path (the path separator is ':' for obvious reasons). The dict for
1109
 
        each key will "inherit" all values from parent paths. However
1110
 
        setting a new value will set it specifically for that key, without
1111
 
        changing the value in the "parents". This is specifically used to store
1112
 
        namespace properties for zim notebooks. So each child namespace will
1113
 
        inherit the properties of it's parents unless it was explicitly
1114
 
        set for that child namespace.
1115
 
 
1116
 
        There is a special member dict stored under the key "__defaults__"
1117
 
        which has the top-level fallback properties.
1118
 
 
1119
 
        Child dicts are auto-vivicated, so this object only implements
1120
 
        C{__getitem__()} but no C{__setitem__()}.
1121
 
        '''
1122
 
        # Note that all the magic is actually implemented by HierarchicDictFrame
1123
 
 
1124
 
        __slots__ = ('dict',)
1125
 
 
1126
 
        def __init__(self, defaults=None):
1127
 
                '''Constructor
1128
 
 
1129
 
                @param defaults: dict with the default properties
1130
 
                '''
1131
 
                self.dict = {}
1132
 
                self.dict['__defaults__'] = defaults or {}
1133
 
 
1134
 
        def __getitem__(self, k):
1135
 
                if not isinstance(k, basestring):
1136
 
                        k = k.name # assume zim path
1137
 
                return HierarchicDictFrame(self.dict, k)
1138
 
 
1139
 
 
1140
 
class HierarchicDictFrame(object):
1141
 
        '''Object acts as a member dict for L{HierarchicDict}'''
1142
 
 
1143
 
        __slots__ = ('dict', 'key')
1144
 
 
1145
 
        def __init__(self, dict, key):
1146
 
                '''Constructor
1147
 
 
1148
 
                @param dict: the dict used to store the properties per namespace
1149
 
                (internal in HierarchicDict)
1150
 
                @param key: the key for this member dict
1151
 
                '''
1152
 
                self.dict = dict
1153
 
                self.key = key
1154
 
 
1155
 
        def _keys(self):
1156
 
                yield self.key
1157
 
                parts = self.key.split(':')
1158
 
                parts.pop()
1159
 
                while parts:
1160
 
                        yield ':'.join(parts)
1161
 
                        parts.pop()
1162
 
                yield '' # top level namespace
1163
 
 
1164
 
        def get(self, k, default=None):
1165
 
                try:
1166
 
                        v = self.__getitem__(k)
1167
 
                except KeyError:
1168
 
                        return default
1169
 
                else:
1170
 
                        return v
1171
 
 
1172
 
        def __getitem__(self, k):
1173
 
                for key in self._keys():
1174
 
                        if key in self.dict and k in self.dict[key]:
1175
 
                                return self.dict[key][k]
1176
 
                else:
1177
 
                        if k in self.dict['__defaults__']:
1178
 
                                return self.dict['__defaults__'][k]
1179
 
                        else:
1180
 
                                raise KeyError
1181
 
 
1182
 
        def __setitem__(self, k, v):
1183
 
                if not self.key in self.dict:
1184
 
                        self.dict[self.key] = {}
1185
 
                self.dict[self.key][k] = v
1186
 
 
1187
 
        def remove(self, k):
1188
 
                if self.key in self.dict and k in self.dict[self.key]:
1189
 
                        return self.dict[self.key].pop(k)
1190
 
                else:
1191
 
                        raise KeyError