qutebrowser/qutebrowser/config/config.py

864 lines
31 KiB
Python
Raw Normal View History

2014-06-19 09:04:37 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2015-01-03 15:51:31 +01:00
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-02-06 14:01:23 +01:00
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
2014-03-27 22:37:34 +01:00
"""Configuration storage and config-related utilities.
This borrows a lot of ideas from configparser, but also has some things that
2015-03-31 20:49:29 +02:00
are fundamentally different. This is why nothing inherits from configparser,
but we borrow some methods and classes from there where it makes sense.
2014-03-27 22:37:34 +01:00
"""
2014-02-17 12:23:52 +01:00
2014-01-27 21:42:00 +01:00
import os
2014-09-28 00:27:22 +02:00
import sys
2014-02-17 12:23:52 +01:00
import os.path
2014-08-27 20:16:04 +02:00
import functools
2014-03-27 22:37:34 +01:00
import configparser
2015-11-10 22:09:36 +01:00
import contextlib
import collections
2014-08-26 19:10:14 +02:00
import collections.abc
2014-01-27 21:42:00 +01:00
2015-02-22 19:13:51 +01:00
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
from qutebrowser.config import configdata, configexc, textwrapper
from qutebrowser.config.parsers import ini, keyconf
from qutebrowser.commands import cmdexc, cmdutils
2015-03-27 07:59:13 +01:00
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
qtutils, error, usertypes)
2014-07-29 00:37:32 +02:00
from qutebrowser.utils.usertypes import Completion
2014-01-28 23:04:02 +01:00
2014-02-19 10:58:32 +01:00
UNSET = object()
class change_filter: # pylint: disable=invalid-name
2015-04-04 22:03:28 +02:00
"""Decorator to filter calls based on a config section/option matching.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
Attributes:
_sectname: The section to be filtered.
_optname: The option to be filtered.
2015-04-30 07:37:25 +02:00
_function: Whether a function rather than a method is decorated.
"""
2015-04-30 07:37:25 +02:00
def __init__(self, sectname, optname=None, function=False):
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
Args:
2015-04-04 22:03:28 +02:00
sectname: The section to be filtered.
optname: The option to be filtered.
2015-04-30 07:37:25 +02:00
function: Whether a function rather than a method is decorated.
"""
if sectname not in configdata.DATA:
raise configexc.NoSectionError(sectname)
if optname is not None and optname not in configdata.DATA[sectname]:
raise configexc.NoOptionError(optname, sectname)
self._sectname = sectname
self._optname = optname
2015-04-30 07:37:25 +02:00
self._function = function
def __call__(self, func):
2015-04-04 22:03:28 +02:00
"""Filter calls to the decorated function.
Gets called when a function should be decorated.
Adds a filter which returns if we're not interested in the change-event
and calls the wrapped function if we are.
We assume the function passed doesn't take any parameters.
Args:
func: The function to be decorated.
Return:
The decorated function.
"""
2015-04-30 07:37:25 +02:00
if self._function:
@pyqtSlot(str, str)
@functools.wraps(func)
def wrapper(sectname=None, optname=None):
# pylint: disable=missing-docstring
if sectname is None and optname is None:
# Called directly, not from a config change event.
return func()
elif sectname != self._sectname:
return
elif self._optname is not None and optname != self._optname:
return
else:
return func()
else:
@pyqtSlot(str, str)
@functools.wraps(func)
def wrapper(wrapper_self, sectname=None, optname=None):
# pylint: disable=missing-docstring
if sectname is None and optname is None:
# Called directly, not from a config change event.
return func(wrapper_self)
elif sectname != self._sectname:
return
elif self._optname is not None and optname != self._optname:
return
else:
return func(wrapper_self)
return wrapper
def get(*args, **kwargs):
"""Convenience method to call get(...) of the config instance."""
2014-09-24 07:06:45 +02:00
return objreg.get('config').get(*args, **kwargs)
2014-05-05 17:56:14 +02:00
def section(sect):
"""Get a config section from the global config."""
2014-09-24 07:06:45 +02:00
return objreg.get('config')[sect]
2015-04-06 00:10:37 +02:00
def _init_main_config(parent=None):
"""Initialize the main config.
Args:
parent: The parent to pass to ConfigManager.
"""
2015-03-27 07:59:13 +01:00
args = objreg.get('args')
2014-09-28 00:27:22 +02:00
try:
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
2015-04-06 00:10:37 +02:00
args.relaxed_config, parent=parent)
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
2014-09-28 00:27:22 +02:00
log.init.exception(e)
errstr = "Error while reading config:"
try:
errstr += "\n\n{} -> {}:".format(
e.section, e.option) # pylint: disable=no-member
except AttributeError:
pass
2015-03-27 07:59:13 +01:00
errstr += "\n"
error.handle_fatal_exc(e, args, "Error while reading config!",
pre_text=errstr)
2014-09-28 00:27:22 +02:00
# We didn't really initialize much so far, so we just quit hard.
2015-03-27 07:59:13 +01:00
sys.exit(usertypes.Exit.err_config)
2014-09-28 00:27:22 +02:00
else:
objreg.register('config', config_obj)
if standarddir.config() is not None:
filename = os.path.join(standarddir.config(), 'qutebrowser.conf')
save_manager = objreg.get('save-manager')
2015-02-13 22:19:45 +01:00
save_manager.add_saveable(
'config', config_obj.save, config_obj.changed,
config_opt=('general', 'auto-save-config'), filename=filename)
for sect in config_obj.sections.values():
for opt in sect.values.values():
if opt.values['conf'] is None:
2015-03-31 20:49:29 +02:00
# Option added to built-in defaults but not in user's
# config yet
save_manager.save('config', explicit=True, force=True)
2015-02-17 22:43:32 +01:00
return
2015-04-06 00:10:37 +02:00
def _init_key_config(parent):
"""Initialize the key config.
Args:
parent: The parent to use for the KeyConfigParser.
"""
2015-03-27 07:59:13 +01:00
args = objreg.get('args')
2014-09-28 00:27:22 +02:00
try:
2015-04-06 00:10:37 +02:00
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
args.relaxed_config,
2015-04-06 00:10:37 +02:00
parent=parent)
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
2014-09-28 00:27:22 +02:00
log.init.exception(e)
errstr = "Error while reading key config:\n"
if e.lineno is not None:
errstr += "In line {}: ".format(e.lineno)
2015-03-27 07:59:13 +01:00
error.handle_fatal_exc(e, args, "Error while reading key config!",
pre_text=errstr)
2014-09-28 00:27:22 +02:00
# We didn't really initialize much so far, so we just quit hard.
2015-03-27 07:59:13 +01:00
sys.exit(usertypes.Exit.err_key_config)
2014-09-28 00:27:22 +02:00
else:
objreg.register('key-config', key_config)
if standarddir.config() is not None:
save_manager = objreg.get('save-manager')
filename = os.path.join(standarddir.config(), 'keys.conf')
2015-02-13 22:19:45 +01:00
save_manager.add_saveable(
'key-config', key_config.save, key_config.config_dirty,
config_opt=('general', 'auto-save-config'), filename=filename,
dirty=key_config.is_dirty)
2015-02-22 19:13:51 +01:00
def _init_misc():
"""Initialize misc. config-related files."""
save_manager = objreg.get('save-manager')
state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state')
for sect in ('general', 'geometry'):
try:
state_config.add_section(sect)
except configparser.DuplicateSectionError:
pass
2015-04-02 14:58:34 +02:00
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
state_config['general'].pop('fooled', None)
2014-09-28 00:27:22 +02:00
objreg.register('state-config', state_config)
save_manager.add_saveable('state-config', state_config.save)
# We need to import this here because lineparser needs config.
from qutebrowser.misc import lineparser
command_history = lineparser.LimitLineParser(
standarddir.data(), 'cmd-history',
limit=('completion', 'cmd-history-max-items'),
parent=objreg.get('config'))
2014-09-28 00:27:22 +02:00
objreg.register('command-history', command_history)
save_manager.add_saveable('command-history', command_history.save,
command_history.changed)
2014-09-28 00:27:22 +02:00
# Set the QSettings path to something like
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
# doesn't overwrite our config.
#
# This fixes one of the corruption issues here:
# https://github.com/The-Compiler/qutebrowser/issues/515
if standarddir.config() is None:
2015-02-20 09:12:56 +01:00
path = os.devnull
else:
path = os.path.join(standarddir.config(), 'qsettings')
for fmt in (QSettings.NativeFormat, QSettings.IniFormat):
QSettings.setPath(fmt, QSettings.UserScope, path)
2014-09-28 00:27:22 +02:00
2015-04-06 00:10:37 +02:00
def init(parent=None):
"""Initialize the config.
Args:
parent: The parent to pass to QObjects which get initialized.
"""
_init_main_config(parent)
_init_key_config(parent)
2015-02-22 19:13:51 +01:00
_init_misc()
def _get_value_transformer(mapping):
"""Get a function which transforms a value for CHANGED_OPTIONS.
Args:
mapping: A dictionary mapping old values to new values. Value is not
transformed if the supplied value doesn't match the old value.
Return:
A function which takes a value and transforms it.
"""
def transformer(val):
try:
return mapping[val]
except KeyError:
return val
return transformer
def _transform_position(val):
"""Transformer for position values."""
mapping = {
'north': 'top',
'south': 'bottom',
'west': 'left',
'east': 'right',
}
try:
return mapping[val]
except KeyError:
return val
2014-04-25 12:07:37 +02:00
class ConfigManager(QObject):
2014-02-24 07:17:17 +01:00
"""Configuration manager for qutebrowser.
2014-02-27 13:11:52 +01:00
Class attributes:
2014-05-01 20:51:07 +02:00
KEY_ESCAPE: Chars which need escaping when they occur as first char
in a line.
ESCAPE_CHAR: The char to be used for escaping
RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'}
RENAMED_OPTIONS: A mapping of renamed options,
{('section', 'oldname'): 'newname'}
CHANGED_OPTIONS: A mapping of arbitrarily changed options,
{('section', 'option'): callable}.
The callable takes the old value and returns the new
one.
DELETED_OPTIONS: A (section, option) list of deleted options.
Attributes:
2014-04-17 14:49:38 +02:00
sections: The configuration data as an OrderedDict.
_fname: The filename to be opened.
_configdir: The dictionary to read the config from and save it in.
2014-03-27 22:37:34 +01:00
_interpolation: An configparser.Interpolation object
_proxies: configparser.SectionProxy objects for sections.
_initialized: Whether the ConfigManager is fully initialized yet.
Signals:
changed: Emitted when a config option changed.
style_changed: When style caches need to be invalidated.
Args: the changed section and option.
"""
2014-05-01 20:51:07 +02:00
KEY_ESCAPE = r'\#['
ESCAPE_CHAR = '\\'
RENAMED_SECTIONS = {
'permissions': 'content'
}
RENAMED_OPTIONS = {
('colors', 'tab.fg.odd'): 'tabs.fg.odd',
('colors', 'tab.fg.even'): 'tabs.fg.even',
('colors', 'tab.fg.selected'): 'tabs.fg.selected.odd',
('colors', 'tabs.fg.selected'): 'tabs.fg.selected.odd',
('colors', 'tab.bg.odd'): 'tabs.bg.odd',
('colors', 'tab.bg.even'): 'tabs.bg.even',
('colors', 'tab.bg.selected'): 'tabs.bg.selected.odd',
('colors', 'tabs.bg.selected'): 'tabs.bg.selected.odd',
('colors', 'tab.bg.bar'): 'tabs.bg.bar',
('colors', 'tab.indicator.start'): 'tabs.indicator.start',
('colors', 'tab.indicator.stop'): 'tabs.indicator.stop',
('colors', 'tab.indicator.error'): 'tabs.indicator.error',
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
2015-01-22 04:50:30 +01:00
('tabs', 'auto-hide'): 'hide-auto',
('completion', 'history-length'): 'cmd-history-max-items',
('colors', 'downloads.fg'): 'downloads.fg.start',
}
DELETED_OPTIONS = [
2015-03-31 20:49:29 +02:00
('colors', 'tab.separator'),
('colors', 'tabs.separator'),
('colors', 'completion.item.bg'),
2015-07-31 18:55:17 +02:00
('tabs', 'indicator-space'),
('tabs', 'hide-auto'),
('tabs', 'hide-always'),
]
CHANGED_OPTIONS = {
2015-06-08 20:48:35 +02:00
('content', 'cookies-accept'):
_get_value_transformer({'default': 'no-3rdparty'}),
('tabs', 'position'): _transform_position,
('ui', 'downloads-position'): _transform_position,
('ui', 'remove-finished-downloads'):
_get_value_transformer({'false': '-1', 'true': '1000'})
}
changed = pyqtSignal(str, str)
style_changed = pyqtSignal(str, str)
2015-03-23 07:58:28 +01:00
def __init__(self, configdir, fname, relaxed=False, parent=None):
super().__init__(parent)
self._initialized = False
self.sections = configdata.data()
2014-08-26 19:10:14 +02:00
self._interpolation = configparser.ExtendedInterpolation()
2014-03-27 22:37:34 +01:00
self._proxies = {}
2015-11-13 22:27:41 +01:00
for sectname in self.sections:
2014-05-05 18:01:43 +02:00
self._proxies[sectname] = SectionProxy(self, sectname)
self._fname = fname
if configdir is None:
self._configdir = None
self._initialized = True
else:
self._configdir = configdir
parser = ini.ReadConfigParser(configdir, fname)
2015-03-23 07:58:28 +01:00
self._from_cp(parser, relaxed)
self._initialized = True
self._validate_all()
2014-02-27 07:50:44 +01:00
2014-02-26 07:44:39 +01:00
def __getitem__(self, key):
"""Get a section from the config."""
2014-03-28 07:18:40 +01:00
return self._proxies[key]
2014-02-26 07:44:39 +01:00
def __repr__(self):
2014-09-26 15:48:24 +02:00
return utils.get_repr(self, fname=self._fname)
def __str__(self):
"""Get the whole config as a string."""
2014-02-27 21:05:51 +01:00
lines = configdata.FIRST_COMMENT.strip('\n').splitlines()
2014-05-05 18:01:43 +02:00
for sectname, sect in self.sections.items():
lines.append('\n[{}]'.format(sectname))
lines += self._str_section_desc(sectname)
lines += self._str_option_desc(sectname, sect)
lines += self._str_items(sect)
2014-03-09 20:13:40 +01:00
return '\n'.join(lines) + '\n'
2014-05-05 18:01:43 +02:00
def _str_section_desc(self, sectname):
"""Get the section description string for sectname."""
wrapper = textwrapper.TextWrapper()
2014-02-27 21:23:06 +01:00
lines = []
2014-05-05 18:01:43 +02:00
seclines = configdata.SECTION_DESC[sectname].splitlines()
2014-02-27 21:23:06 +01:00
for secline in seclines:
2014-04-22 08:42:47 +02:00
if 'http://' in secline or 'https://' in secline:
2014-02-27 21:23:06 +01:00
lines.append('# ' + secline)
else:
lines += wrapper.wrap(secline)
return lines
2014-05-05 18:01:43 +02:00
def _str_option_desc(self, sectname, sect):
"""Get the option description strings for sect/sectname."""
wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5,
subsequent_indent='#' + ' ' * 5)
2014-02-27 21:23:06 +01:00
lines = []
2014-05-05 18:01:43 +02:00
if not getattr(sect, 'descriptions', None):
2014-02-27 22:13:26 +01:00
return lines
2014-05-05 18:01:43 +02:00
for optname, option in sect.items():
2014-02-27 22:29:25 +01:00
lines.append('#')
if option.typ.special:
2014-02-27 22:13:26 +01:00
typestr = ''
else:
typestr = ' ({})'.format(option.typ.__class__.__name__)
2014-04-25 16:53:23 +02:00
lines.append("# {}{}:".format(optname, typestr))
2014-02-27 21:23:06 +01:00
try:
2014-05-05 18:01:43 +02:00
desc = self.sections[sectname].descriptions[optname]
except KeyError:
2015-04-13 08:49:04 +02:00
log.config.exception("No description for {}.{}!".format(
sectname, optname))
2014-02-27 21:23:06 +01:00
continue
for descline in desc.splitlines():
2014-02-27 22:29:25 +01:00
lines += wrapper.wrap(descline)
2014-02-27 22:13:26 +01:00
valid_values = option.typ.valid_values
if valid_values is not None:
2014-02-28 15:10:34 +01:00
if valid_values.descriptions:
for val in valid_values:
desc = valid_values.descriptions[val]
2014-04-25 16:53:23 +02:00
lines += wrapper.wrap(" {}: {}".format(val, desc))
2014-02-28 15:10:34 +01:00
else:
2014-04-25 16:53:23 +02:00
lines += wrapper.wrap("Valid values: {}".format(', '.join(
2014-02-27 22:29:25 +01:00
valid_values)))
lines += wrapper.wrap("Default: {}".format(
option.values['default']))
2014-02-27 21:23:06 +01:00
return lines
2014-05-05 18:01:43 +02:00
def _str_items(self, sect):
"""Get the option items as string for sect."""
2014-02-27 21:23:06 +01:00
lines = []
2014-05-05 18:01:43 +02:00
for optname, option in sect.items():
value = option.value(startlayer='conf')
2014-05-01 20:51:07 +02:00
for c in self.KEY_ESCAPE:
if optname.startswith(c):
optname = optname.replace(c, self.ESCAPE_CHAR + c, 1)
2014-05-11 21:55:41 +02:00
# configparser can't handle = in keys :(
optname = optname.replace('=', '<eq>')
2014-05-01 20:51:07 +02:00
keyval = '{} = {}'.format(optname, value)
2014-03-21 16:50:37 +01:00
lines.append(keyval)
2014-02-27 21:23:06 +01:00
return lines
2014-12-15 22:25:06 +01:00
def _get_real_sectname(self, cp, sectname):
"""Get an old or new section name based on a configparser.
This checks if sectname is in cp, and if not, migrates it if needed and
tries again.
Args:
cp: The configparser to check.
sectname: The new section name.
Returns:
The section name in the configparser as a string, or None if the
configparser doesn't contain the section.
"""
reverse_renamed_sections = {v: k for k, v in
self.RENAMED_SECTIONS.items()}
if sectname in reverse_renamed_sections:
old_sectname = reverse_renamed_sections[sectname]
else:
old_sectname = sectname
if old_sectname in cp:
return old_sectname
elif sectname in cp:
return sectname
else:
return None
2015-03-23 07:58:28 +01:00
def _from_cp(self, cp, relaxed=False):
"""Read the config from a configparser instance.
Args:
2014-04-21 21:11:01 +02:00
cp: The configparser instance to read the values from.
2015-03-23 08:19:31 +01:00
relaxed: Whether to ignore inexistent sections/options.
"""
for sectname in cp:
if sectname in self.RENAMED_SECTIONS:
sectname = self.RENAMED_SECTIONS[sectname]
if sectname is not 'DEFAULT' and sectname not in self.sections:
2015-03-23 07:58:28 +01:00
if not relaxed:
raise configexc.NoSectionError(sectname)
for sectname in self.sections:
2015-03-23 08:19:31 +01:00
self._from_cp_section(sectname, cp, relaxed)
def _from_cp_section(self, sectname, cp, relaxed):
"""Read a single section from a configparser instance.
Args:
sectname: The name of the section to read.
cp: The configparser instance to read the values from.
relaxed: Whether to ignore inexistent options.
"""
real_sectname = self._get_real_sectname(cp, sectname)
if real_sectname is None:
return
for k, v in cp[real_sectname].items():
if k.startswith(self.ESCAPE_CHAR):
k = k[1:]
2015-03-23 08:19:31 +01:00
if (sectname, k) in self.DELETED_OPTIONS:
return
if (sectname, k) in self.RENAMED_OPTIONS:
2015-03-23 08:19:31 +01:00
k = self.RENAMED_OPTIONS[sectname, k]
if (sectname, k) in self.CHANGED_OPTIONS:
func = self.CHANGED_OPTIONS[(sectname, k)]
v = func(v)
2015-03-23 08:19:31 +01:00
try:
self.set('conf', sectname, k, v, validate=False)
except configexc.NoOptionError:
if relaxed:
pass
else:
raise
def _validate_all(self):
"""Validate all values set in self._from_cp."""
for sectname, sect in self.sections.items():
mapping = {key: val.value() for key, val in sect.values.items()}
for optname, opt in sect.items():
interpolated = self._interpolation.before_get(
self, sectname, optname, opt.value(), mapping)
try:
opt.typ.validate(interpolated)
except configexc.ValidationError as e:
e.section = sectname
e.option = optname
raise
def _changed(self, sectname, optname):
"""Notify other objects the config has changed."""
2015-04-13 08:49:04 +02:00
log.config.debug("Config option changed: {} -> {}".format(
sectname, optname))
if sectname in ('colors', 'fonts'):
self.style_changed.emit(sectname, optname)
self.changed.emit(sectname, optname)
def _after_set(self, changed_sect, changed_opt):
"""Clean up caches and emit signals after an option has been set."""
self.get.cache_clear()
self._changed(changed_sect, changed_opt)
# Options in the same section and ${optname} interpolation.
for optname, option in self.sections[changed_sect].items():
if '${' + changed_opt + '}' in option.value():
self._changed(changed_sect, optname)
# Options in any section and ${sectname:optname} interpolation.
2014-09-15 22:01:13 +02:00
for sectname, sect in self.sections.items():
for optname, option in sect.items():
if ('${' + changed_sect + ':' + changed_opt + '}' in
option.value()):
self._changed(sectname, optname)
def items(self, sectname, raw=True):
"""Get a list of (optname, value) tuples for a section.
2015-03-31 20:49:29 +02:00
Implemented for configparser interpolation compatibility
Args:
sectname: The name of the section to get.
raw: Whether to get raw values. Note this parameter only exists
for ConfigParser compatibility and raw=False is not supported.
"""
items = []
if not raw:
raise ValueError("items() with raw=True is not implemented!")
for optname, option in self.sections[sectname].items():
items.append((optname, option.value()))
return items
2014-05-05 18:37:40 +02:00
def has_option(self, sectname, optname):
2014-04-10 12:37:49 +02:00
"""Check if option exists in section.
2014-04-17 17:44:27 +02:00
Args:
2014-05-05 18:01:43 +02:00
sectname: The section name.
2014-05-05 18:37:40 +02:00
optname: The option name
2014-04-10 12:37:49 +02:00
Return:
True if the option and section exist, False otherwise.
2014-04-10 12:24:41 +02:00
"""
2014-05-05 18:01:43 +02:00
if sectname not in self.sections:
return False
2014-05-05 18:37:40 +02:00
return optname in self.sections[sectname]
2014-04-07 17:53:57 +02:00
2014-05-05 18:37:40 +02:00
def remove_option(self, sectname, optname):
2014-04-10 12:37:49 +02:00
"""Remove an option.
2014-04-17 17:44:27 +02:00
Args:
2014-05-05 18:01:43 +02:00
sectname: The section where to remove an option.
2014-05-05 18:37:40 +02:00
optname: The option name to remove.
2014-04-10 12:37:49 +02:00
Return:
True if the option existed, False otherwise.
"""
try:
2014-05-05 18:01:43 +02:00
sectdict = self.sections[sectname]
2014-04-10 12:37:49 +02:00
except KeyError:
raise configexc.NoSectionError(sectname)
2014-05-05 18:37:40 +02:00
optname = self.optionxform(optname)
existed = optname in sectdict
2014-04-10 12:37:49 +02:00
if existed:
2014-05-05 18:37:40 +02:00
del sectdict[optname]
self.get.cache_clear()
2014-04-10 12:37:49 +02:00
return existed
2014-08-27 20:16:04 +02:00
@functools.lru_cache()
def get(self, sectname, optname, raw=False, transformed=True,
fallback=UNSET):
2014-03-28 07:18:40 +01:00
"""Get the value from a section/option.
We don't support the vars argument from configparser.get as it's not
hashable.
2014-04-17 17:44:27 +02:00
Args:
2014-05-05 18:01:43 +02:00
sectname: The section to get the option from.
2014-05-05 18:37:40 +02:00
optname: The option name
2014-04-02 16:47:21 +02:00
raw: Whether to get the uninterpolated, untransformed value.
2014-05-01 20:06:34 +02:00
transformed: Whether the value should be transformed.
2014-04-17 17:44:27 +02:00
Return:
The value of the option.
2014-03-28 07:18:40 +01:00
"""
if not self._initialized:
2015-03-31 20:49:29 +02:00
raise Exception("get got called before initialization was "
"complete!")
2014-02-26 07:44:39 +01:00
try:
2014-05-05 18:01:43 +02:00
sect = self.sections[sectname]
except KeyError:
if fallback is not UNSET:
return fallback
raise configexc.NoSectionError(sectname)
try:
2014-05-05 18:37:40 +02:00
val = sect[optname]
2014-02-26 07:44:39 +01:00
except KeyError:
if fallback is not UNSET:
return fallback
raise configexc.NoOptionError(optname, sectname)
if raw:
return val.value()
mapping = {key: val.value() for key, val in sect.values.items()}
2014-05-05 18:37:40 +02:00
newval = self._interpolation.before_get(self, sectname, optname,
val.value(), mapping)
2014-05-01 20:06:34 +02:00
if transformed:
newval = val.typ.transform(newval)
return newval
2014-02-26 07:44:39 +01:00
2015-11-10 22:09:36 +01:00
@contextlib.contextmanager
def _handle_config_error(self):
"""Catch errors in set_command and raise CommandError."""
try:
yield
except (configexc.NoOptionError, configexc.NoSectionError,
configexc.ValidationError) as e:
raise cmdexc.CommandError("set: {}".format(e))
except (configexc.Error, configparser.Error) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
@cmdutils.register(name='set', instance='config', win_id='win_id',
2014-07-29 00:37:32 +02:00
completion=[Completion.section, Completion.option,
Completion.value])
def set_command(self, win_id, section_=None, option=None, value=None,
temp=False, print_=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
If the option name ends with '!' and it is a boolean value, toggle it.
//
Wrapper for self.set() to output exceptions in the status bar.
2014-08-03 00:33:39 +02:00
Args:
section_: The section where the option is in.
option: The name of the option.
2014-08-03 00:33:39 +02:00
value: The value to set.
2014-09-13 00:22:27 +02:00
temp: Set value temporarily.
print_: Print the value after setting.
"""
if section_ is not None and option is None:
raise cmdexc.CommandError(
"set: Either both section and option have to be given, or "
"neither!")
if section_ is None and option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
return
2015-11-10 21:27:42 +01:00
if option.endswith('?') and option != '?':
option = option[:-1]
print_ = True
else:
2015-11-10 22:09:36 +01:00
with self._handle_config_error():
2015-11-10 21:27:42 +01:00
if option.endswith('!') and option != '!' and value is None:
2015-08-04 23:50:43 +02:00
option = option[:-1]
val = self.get(section_, option)
layer = 'temp' if temp else 'conf'
if isinstance(val, bool):
2015-08-04 23:50:43 +02:00
self.set(layer, section_, option, str(not val))
else:
raise cmdexc.CommandError(
"set: Attempted inversion of non-boolean value.")
elif value is not None:
layer = 'temp' if temp else 'conf'
self.set(layer, section_, option, value)
else:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
if print_:
2015-11-10 22:09:36 +01:00
with self._handle_config_error():
2015-11-10 21:27:42 +01:00
val = self.get(section_, option, transformed=False)
message.info(win_id, "{} {} = {}".format(
section_, option, val), immediately=True)
def set(self, layer, sectname, optname, value, validate=True):
"""Set an option.
Args:
2014-04-17 17:44:27 +02:00
layer: A layer name as string (conf/temp/default).
2014-05-05 18:01:43 +02:00
sectname: The name of the section to change.
2014-05-05 18:37:40 +02:00
optname: The name of the option to change.
value: The new value.
validate: Whether to validate the value immediately.
"""
try:
value = self._interpolation.before_set(self, sectname, optname,
value)
except ValueError as e:
raise configexc.InterpolationSyntaxError(optname, sectname, str(e))
2014-04-10 07:09:12 +02:00
try:
2014-05-05 18:01:43 +02:00
sect = self.sections[sectname]
2014-04-10 07:09:12 +02:00
except KeyError:
raise configexc.NoSectionError(sectname)
mapping = {key: val.value() for key, val in sect.values.items()}
if validate:
interpolated = self._interpolation.before_get(
self, sectname, optname, value, mapping)
else:
interpolated = None
2014-04-10 12:03:42 +02:00
try:
2014-05-05 18:37:40 +02:00
sect.setv(layer, optname, value, interpolated)
2014-04-10 12:03:42 +02:00
except KeyError:
raise configexc.NoOptionError(optname, sectname)
else:
if self._initialized:
self._after_set(sectname, optname)
2014-04-09 22:44:07 +02:00
2014-02-26 07:44:39 +01:00
def save(self):
2014-02-26 09:18:27 +01:00
"""Save the config file."""
if self._configdir is None:
return
2014-10-02 22:51:59 +02:00
configfile = os.path.join(self._configdir, self._fname)
log.destroy.debug("Saving config to {}".format(configfile))
with qtutils.savefile_open(configfile) as f:
f.write(str(self))
2014-02-26 07:44:39 +01:00
def dump_userconfig(self):
2014-02-26 09:18:27 +01:00
"""Get the part of the config which was changed by the user.
Return:
The changed config part as string.
"""
2014-04-10 07:37:13 +02:00
lines = []
2014-05-05 18:01:43 +02:00
for sectname, sect in self.sections.items():
changed = sect.dump_userconfig()
2014-04-17 12:35:46 +02:00
if changed:
2014-05-05 18:01:43 +02:00
lines.append('[{}]'.format(sectname))
2014-04-17 12:35:46 +02:00
lines += ['{} = {}'.format(k, v) for k, v in changed]
if not lines:
lines = ['<Default configuration>']
2014-04-10 07:37:13 +02:00
return '\n'.join(lines)
2014-02-26 07:44:39 +01:00
2014-04-07 16:51:14 +02:00
def optionxform(self, val):
"""Implemented to be compatible with ConfigParser interpolation."""
return val
2014-02-26 07:44:39 +01:00
2014-08-26 19:10:14 +02:00
class SectionProxy(collections.abc.MutableMapping):
2014-04-07 17:20:14 +02:00
2014-04-10 12:37:49 +02:00
"""A proxy for a single section from a config.
Attributes:
_conf: The Config object.
_name: The section name.
"""
2014-03-27 22:37:34 +01:00
2014-04-10 12:37:49 +02:00
def __init__(self, conf, name):
"""Create a view on a section.
2014-04-17 17:44:27 +02:00
Args:
2014-04-10 12:37:49 +02:00
conf: The Config object.
name: The section name.
"""
self.conf = conf
self.name = name
2014-04-10 12:37:49 +02:00
def __repr__(self):
2014-09-26 15:48:24 +02:00
return utils.get_repr(self, name=self.name)
2014-04-10 12:37:49 +02:00
2014-03-27 22:37:34 +01:00
def __getitem__(self, key):
if not self.conf.has_option(self.name, key):
2014-04-10 12:37:49 +02:00
raise KeyError(key)
return self.conf.get(self.name, key)
2014-03-27 22:37:34 +01:00
def __setitem__(self, key, value):
return self.conf.set('conf', self.name, key, value)
2014-03-27 22:37:34 +01:00
def __delitem__(self, key):
if not (self.conf.has_option(self.name, key) and
self.conf.remove_option(self.name, key)):
2014-04-10 12:37:49 +02:00
raise KeyError(key)
2014-03-27 22:37:34 +01:00
def __contains__(self, key):
return self.conf.has_option(self.name, key)
2014-03-27 22:37:34 +01:00
2014-04-10 12:37:49 +02:00
def __len__(self):
return len(self._options())
2014-03-27 22:37:34 +01:00
2014-04-10 12:37:49 +02:00
def __iter__(self):
return self._options().__iter__()
2014-03-27 22:37:34 +01:00
2014-04-10 12:37:49 +02:00
def _options(self):
"""Get the option keys from this section."""
return self.conf.sections[self.name].keys()
2014-03-27 22:37:34 +01:00
2014-08-04 03:47:09 +02:00
def get(self, optname, *, raw=False): # pylint: disable=arguments-differ
2014-04-10 12:37:49 +02:00
"""Get a value from this section.
2014-03-27 22:37:34 +01:00
2014-04-10 14:40:02 +02:00
We deliberately don't support the default argument here, but have a raw
argument instead.
2014-04-17 17:44:27 +02:00
Args:
2014-05-05 18:37:40 +02:00
optname: The option name to get.
2014-04-10 12:37:49 +02:00
raw: Whether to get a raw value or not.
"""
return self.conf.get(self.name, optname, raw=raw)