config.py 11.2 KB
Newer Older
louiz’'s avatar
louiz’ committed
1
# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
2 3 4 5
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
6
# it under the terms of the zlib license. See the COPYING file.
7

8 9 10 11 12
"""
Defines the global config instance, used to get or set (and save) values
from/to the config file
"""

13 14
DEFSECTION = "Poezio"

mathieui's avatar
mathieui committed
15
import logging
mathieui's avatar
mathieui committed
16 17
import os
import sys
mathieui's avatar
mathieui committed
18

19
from configparser import RawConfigParser, NoOptionError, NoSectionError
20
from os import environ, makedirs, path, remove
21
from shutil import copy2
22
from args import parse_args
23 24 25 26 27 28 29

class Config(RawConfigParser):
    """
    load/save the config to a file
    """
    def __init__(self, file_name):
        RawConfigParser.__init__(self, None)
30 31
        # make the options case sensitive
        self.optionxform = str
32 33 34 35
        self.read_file(file_name)

    def read_file(self, file_name):
        self.file_name = file_name
mathieui's avatar
mathieui committed
36 37 38 39
        try:
            RawConfigParser.read(self, file_name, encoding='utf-8')
        except TypeError: # python < 3.2 sucks
            RawConfigParser.read(self, file_name)
40 41 42 43
        # Check config integrity and fix it if it’s wrong
        for section in ('bindings', 'var'):
            if not self.has_section(section):
                self.add_section(section)
44

45
    def get(self, option, default, section=DEFSECTION):
46 47 48 49 50 51
        """
        get a value from the config but return
        a default value if it is not found
        The type of default defines the type
        returned
        """
52
        try:
53
            if type(default) == int:
54
                res = self.getint(option, section)
55
            elif type(default) == float:
56
                res = self.getfloat(option, section)
57
            elif type(default) == bool:
58
                res = self.getboolean(option, section)
59
            else:
60
                res = self.getstr(option, section)
61 62 63
        except (NoOptionError, NoSectionError, ValueError, AttributeError) as e:
            return default
        if res is None:
64 65 66
            return default
        return res

67
    def get_by_tabname(self, option, default, tabname, fallback=True, fallback_server=True):
68 69 70 71 72 73 74 75 76 77
        """
        Try to get the value for the option. First we look in
        a section named `tabname`, if the option is not present
        in the section, we search for the global option if fallback is
        True. And we return `default` as a fallback as a last resort.
        """
        if tabname in self.sections():
            if option in self.options(tabname):
                # We go the tab-specific option
                return self.get(option, default, tabname)
78 79
        if fallback_server:
            return self.get_by_servname(tabname, option, default, fallback)
80 81 82 83 84
        if fallback:
            # We fallback to the global option
            return self.get(option, default)
        return default

85 86 87 88 89 90 91 92 93 94 95 96 97 98
    def get_by_servname(self, jid, option, default, fallback=True):
        """
        Try to get the value of an option for a server
        """
        server = safeJID(jid).server
        if server:
            server = '@' + server
            if server in self.sections() and option in self.options(server):
                return self.get(option, default, server)
        if fallback:
            return self.get(option, default)
        return default


99
    def __get(self, option, section=DEFSECTION, **kwargs):
100 101 102
        """
        facility for RawConfigParser.get
        """
103 104 105 106 107 108 109
        return RawConfigParser.get(self, section, option, **kwargs)

    def _get(self, section, conv, option, **kwargs):
        """
        Redirects RawConfigParser._get
        """
        return conv(self.__get(option, section, **kwargs))
110

111
    def getstr(self, option, section=DEFSECTION):
112 113 114
        """
        get a value and returns it as a string
        """
115
        return self.__get(option, section)
116

117
    def getint(self, option, section=DEFSECTION):
118 119 120
        """
        get a value and returns it as an int
        """
121
        return RawConfigParser.getint(self, section, option)
122

123
    def getfloat(self, option, section=DEFSECTION):
124 125 126
        """
        get a value and returns it as a float
        """
127
        return RawConfigParser.getfloat(self, section, option)
128

129
    def getboolean(self, option, section=DEFSECTION):
130 131 132
        """
        get a value and returns it as a boolean
        """
133
        return RawConfigParser.getboolean(self, section, option)
134

135 136 137 138 139
    def write_in_file(self, section, option, value):
        """
        Our own way to save write the value in the file
        Just find the right section, and then find the
        right option, and edit it.
140 141 142

        TODO: make it write also new values in the file, not just what did already
        exist
143
        """
144
        if path.exists(self.file_name):
145
            df = open(self.file_name, 'r', encoding='utf-8')
146 147 148 149
            lines_before = (line.strip() for line in df.readlines())
            df.close()
        else:
            lines_before = []
150 151
        result_lines = []
        we_are_in_the_right_section = False
152 153
        written = False
        section_found = False
154 155
        for line in lines_before:
            if line.startswith('['): # check the section
156
                if we_are_in_the_right_section and not written:
mathieui's avatar
mathieui committed
157
                    result_lines.append('%s = %s' % (option, value))
158
                    written = True
159 160
                if line == '[%s]' % section:
                    we_are_in_the_right_section = True
161
                    section_found = True
162 163
                else:
                    we_are_in_the_right_section = False
164
            if (line.startswith('%s ' % (option,)) or
165 166
                line.startswith('%s=' % (option,)) or
                line.startswith('%s = ' % (option,))) and we_are_in_the_right_section:
mathieui's avatar
mathieui committed
167
                line = '%s = %s' % (option, value)
168
                written = True
169
            result_lines.append(line)
170 171 172

        if not section_found:
            result_lines.append('[%s]' % section)
mathieui's avatar
mathieui committed
173
            result_lines.append('%s = %s' % (option, value))
174
        elif not written:
mathieui's avatar
mathieui committed
175
            result_lines.append('%s = %s' % (option, value))
mathieui's avatar
mathieui committed
176
        try:
177 178 179 180 181 182 183 184
            prefix, file = path.split(self.file_name)
            filename = path.join(prefix, '.%s.tmp' % file)
            fd = os.fdopen(
                    os.open(
                        filename,
                        os.O_WRONLY | os.O_CREAT,
                        0o600),
                    'w')
mathieui's avatar
mathieui committed
185
            for line in result_lines:
186 187 188 189
                fd.write('%s\n' % line)
            fd.close()
            copy2(filename, self.file_name)
            remove(filename)
mathieui's avatar
mathieui committed
190 191
        except:
            success = False
192
            log.error('Unable to save the config file.', exc_info=True)
mathieui's avatar
mathieui committed
193 194 195
        else:
            success = True
        return success
196

197
    def set_and_save(self, option, value, section=DEFSECTION):
198 199 200 201
        """
        set the value in the configuration then save it
        to the file
        """
202 203 204 205 206
        # Special case for a 'toggle' value. We take the current value
        # and set the opposite. Warning if the no current value exists
        # or it is not a bool.
        if value == "toggle":
            current = self.get(option, "", section)
207 208
            if isinstance(current, bool):
                value = str(not current)
209
            else:
210 211 212 213 214 215
                if current.lower() == "false":
                    value = "true"
                elif current.lower() == "true":
                    value = "false"
                else:
                    return (_("Could not toggle option: %s. Current value is %s.") % (option, current or _("empty")), 'Warning')
216 217 218 219
        if self.has_section(section):
            RawConfigParser.set(self, section, option, value)
        else:
            self.add_section(section)
220
            RawConfigParser.set(self, section, option, value)
mathieui's avatar
mathieui committed
221 222 223
        if not self.write_in_file(section, option, value):
            return (_('Unable to write in the config file'), 'Error')
        return ("%s=%s" % (option, value), 'Info')
224

mathieui's avatar
mathieui committed
225 226 227 228 229 230 231 232 233 234
    def silent_set(self, option, value, section=DEFSECTION):
        """
        Set a value, save, and return True on success and False on failure
        """
        if self.has_section(section):
            RawConfigParser.set(self, section, option, value)
        else:
            self.add_section(section)
            RawConfigParser.set(self, section, option, value)
        return self.write_in_file(section, option, value)
235

236 237 238 239 240 241 242 243 244
    def set(self, option, value, section=DEFSECTION):
        """
        Set the value of an option temporarily
        """
        try:
            RawConfigParser.set(self, section, option, value)
        except NoSectionError:
            pass

245 246 247 248 249 250 251 252 253 254
    def to_dict(self):
        """
        Returns a dict of the form {section: {option: value, option: value}, …}
        """
        res = {}
        for section in self.sections():
            res[section] = {}
            for option in self.options(section):
                res[section][option] = self.get(option, "", section)
        return res
255 256 257

firstrun = False

258 259
# creates the configuration directory if it doesn't exist
# and copy the default config in it
260 261
CONFIG_HOME = environ.get("XDG_CONFIG_HOME")
if not CONFIG_HOME:
262
    CONFIG_HOME = path.join(environ.get('HOME'), '.config')
263
CONFIG_PATH = path.join(CONFIG_HOME, 'poezio')
mathieui's avatar
mathieui committed
264

265
try:
266
    makedirs(CONFIG_PATH)
267 268
except OSError:
    pass
269

mathieui's avatar
mathieui committed
270 271 272 273
options = parse_args(CONFIG_PATH)

# Copy a default file if none exists
if not path.isfile(options.filename):
mathieui's avatar
mathieui committed
274
    default = path.join(path.dirname(__file__), '../data/default_config.cfg')
275
    other = path.join(path.dirname(__file__), 'default_config.cfg')
mathieui's avatar
mathieui committed
276 277 278 279
    if path.isfile(default):
        copy2(default, options.filename)
    elif path.isfile(other):
        copy2(other, options.filename)
280
    firstrun = True
281

282 283 284 285 286 287 288
try:
    config = Config(options.filename)
except:
    import traceback
    sys.stderr.write('Poezio was unable to read or parse the config file.\n')
    traceback.print_exc(limit=0)
    sys.exit(1)
289 290 291 292 293 294 295 296 297 298 299 300 301 302

LOG_DIR = config.get('log_dir', '') or path.join(environ.get('XDG_DATA_HOME') or path.join(environ.get('HOME'), '.local', 'share'), 'poezio')
LOG_DIR = path.expanduser(LOG_DIR)

try:
    makedirs(LOG_DIR)
except:
    pass

LOGGING_CONFIG = {
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        'simple': {
303
            'format': '%(asctime)s %(levelname)s:%(module)s:%(message)s'
304 305 306 307 308 309 310 311 312 313
        }
    },
    'handlers': {
    },
    'root': {
            'handlers': [],
            'propagate': True,
            'level': 'DEBUG',
    }
}
314
if config.get('log_errors', True):
315
    LOGGING_CONFIG['root']['handlers'].append('error')
316 317 318 319 320 321
    LOGGING_CONFIG['handlers']['error'] = {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': path.join(LOG_DIR, 'errors.log'),
            'formatter': 'simple',
        }
322 323 324

if options.debug:
    LOGGING_CONFIG['root']['handlers'].append('debug')
325 326 327 328 329 330 331
    LOGGING_CONFIG['handlers']['debug'] = {
            'level':'DEBUG',
            'class':'logging.FileHandler',
            'filename': options.debug,
            'formatter': 'simple',
        }

332 333 334 335 336 337 338 339 340 341

if LOGGING_CONFIG['root']['handlers']:
    logging.config.dictConfig(LOGGING_CONFIG)
else:
    logging.basicConfig(level=logging.CRITICAL)

# common import sleekxmpp, which creates then its loggers, so
# it needs to be after logger configuration
from common import safeJID

342 343
log = logging.getLogger(__name__)