qutebrowser/qutebrowser/config/config.py

471 lines
16 KiB
Python
Raw Normal View History

2014-02-06 14:01:23 +01:00
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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
are fundamentally different. This is why nothing inherts from configparser, but
we borrow some methods and classes from there where it makes sense.
2014-04-17 17:44:27 +02:00
Module attributes:
instance: The "qutebrowser.conf" Config instance.
state: The "state" ReadWriteConfigParser instance.
cmd_history: The "cmd_history" LineConfigParser instance.
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-02-17 12:23:52 +01:00
import os.path
2014-01-27 21:42:00 +01:00
import logging
2014-02-27 07:18:36 +01:00
import textwrap
2014-03-27 22:37:34 +01:00
import configparser
2014-04-16 16:16:17 +02:00
from configparser import ExtendedInterpolation
2014-04-10 12:37:49 +02:00
from collections.abc import MutableMapping
2014-01-27 21:42:00 +01:00
2014-04-16 16:16:17 +02:00
from PyQt5.QtCore import pyqtSignal, QObject
2014-02-27 18:46:30 +01:00
import qutebrowser.config.configdata as configdata
2014-03-09 19:25:15 +01:00
import qutebrowser.commands.utils as cmdutils
2014-04-09 20:47:24 +02:00
import qutebrowser.utils.message as message
from qutebrowser.config._conftypes import ValidationError
from qutebrowser.config._iniparsers import (ReadConfigParser,
ReadWriteConfigParser)
from qutebrowser.config._lineparser import LineConfigParser
2014-02-18 10:07:52 +01:00
2014-04-17 17:44:27 +02:00
instance = None
2014-02-18 11:57:35 +01:00
state = None
2014-04-15 18:02:07 +02:00
cmd_history = None
2014-01-28 12:21:00 +01:00
2014-01-28 23:04:02 +01:00
def init(configdir):
2014-02-19 10:58:32 +01:00
"""Initialize the global objects based on the config in configdir.
Args:
configdir: The directory where the configs are stored in.
2014-02-19 10:58:32 +01:00
"""
global instance, state, cmd_history
logging.debug("Config init, configdir {}".format(configdir))
2014-04-25 12:07:37 +02:00
instance = ConfigManager(configdir, 'qutebrowser.conf')
state = ReadWriteConfigParser(configdir, 'state')
2014-04-15 18:02:07 +02:00
cmd_history = LineConfigParser(configdir, 'cmd_history',
2014-04-27 21:21:14 +02:00
('completion', 'history-length'))
2014-02-10 07:03:51 +01:00
2014-02-18 14:21:39 +01:00
def get(*args, **kwargs):
"""Convenience method to call get(...) of the config instance."""
return instance.get(*args, **kwargs)
2014-04-16 16:16:17 +02:00
class NoSectionError(configparser.NoSectionError):
"""Exception raised when a section was not found."""
pass
class NoOptionError(configparser.NoOptionError):
"""Exception raised when an option was not found."""
pass
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.
VALUE_ESCAPE: Chars to escape inside values.
ESCAPE_CHAR: The char to be used for escaping
Attributes:
2014-04-17 14:49:38 +02:00
sections: The configuration data as an OrderedDict.
2014-03-10 00:37:35 +01:00
_configparser: A ReadConfigParser instance to load the config.
_wrapper_args: A dict with the default kwargs for the config wrappers.
_configdir: The dictionary to read the config from and save it in.
_configfile: The config file path.
2014-03-27 22:37:34 +01:00
_interpolation: An configparser.Interpolation object
_proxies: configparser.SectionProxy objects for sections.
Signals:
changed: Gets emitted when the config has changed.
2014-04-16 09:21:27 +02:00
Args: the changed section, option and new value.
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'\#['
VALUE_ESCAPE = r'\$'
ESCAPE_CHAR = '\\'
changed = pyqtSignal(str, str)
style_changed = pyqtSignal(str, str)
def __init__(self, configdir, fname, parent=None):
super().__init__(parent)
2014-04-17 17:44:27 +02:00
self.sections = configdata.DATA
self._configparser = ReadConfigParser(configdir, fname)
self._configfile = os.path.join(configdir, fname)
2014-02-27 21:23:06 +01:00
self._wrapper_args = {
'width': 72,
'replace_whitespace': False,
'break_long_words': False,
'break_on_hyphens': False,
}
self._configdir = configdir
2014-03-27 22:37:34 +01:00
self._interpolation = ExtendedInterpolation()
self._proxies = {}
for secname in self.sections.keys():
2014-03-27 22:37:34 +01:00
self._proxies[secname] = SectionProxy(self, secname)
self._from_cp(self._configparser)
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 __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-04-17 14:49:38 +02:00
for secname, section in self.sections.items():
2014-02-27 21:05:51 +01:00
lines.append('\n[{}]'.format(secname))
2014-02-27 21:23:06 +01:00
lines += self._str_section_desc(secname)
lines += self._str_option_desc(secname, section)
lines += self._str_items(section)
2014-03-09 20:13:40 +01:00
return '\n'.join(lines) + '\n'
2014-02-27 21:23:06 +01:00
def _str_section_desc(self, secname):
2014-02-27 23:21:21 +01:00
"""Get the section description string for secname."""
2014-02-27 21:23:06 +01:00
wrapper = textwrap.TextWrapper(initial_indent='# ',
subsequent_indent='# ',
**self._wrapper_args)
lines = []
seclines = configdata.SECTION_DESC[secname].splitlines()
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
def _str_option_desc(self, secname, section):
2014-02-27 23:21:21 +01:00
"""Get the option description strings for section/secname."""
2014-02-27 21:23:06 +01:00
wrapper = textwrap.TextWrapper(initial_indent='#' + ' ' * 5,
subsequent_indent='#' + ' ' * 5,
**self._wrapper_args)
lines = []
2014-04-07 17:05:51 +02:00
if not getattr(section, 'descriptions', None):
2014-02-27 22:13:26 +01:00
return lines
2014-02-27 21:23:06 +01:00
for optname, option in section.items():
2014-02-27 22:29:25 +01:00
lines.append('#')
2014-02-27 22:13:26 +01:00
if option.typ.typestr is None:
typestr = ''
else:
typestr = ' ({})'.format(option.typ.typestr)
2014-04-25 16:53:23 +02:00
lines.append("# {}{}:".format(optname, typestr))
2014-02-27 21:23:06 +01:00
try:
2014-04-17 14:49:38 +02:00
desc = self.sections[secname].descriptions[optname]
2014-02-27 21:23:06 +01:00
except KeyError:
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)))
2014-04-25 16:53:23 +02:00
lines += wrapper.wrap("Default: {}".format(
option.values['default']))
2014-02-27 21:23:06 +01:00
return lines
def _str_items(self, section):
2014-02-27 23:21:21 +01:00
"""Get the option items as string for section."""
2014-02-27 21:23:06 +01:00
lines = []
for optname, option in section.items():
2014-05-01 20:51:07 +02:00
value = option.get_first_value(startlayer='conf')
for c in self.KEY_ESCAPE:
if optname.startswith(c):
optname = optname.replace(c, self.ESCAPE_CHAR + c, 1)
2014-05-01 20:51:07 +02:00
for c in self.VALUE_ESCAPE:
value = value.replace(c, self.ESCAPE_CHAR + c)
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
def _from_cp(self, cp):
"""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.
"""
for secname in self.sections.keys():
if secname not in cp:
continue
for k, v in cp[secname].items():
if k.startswith(self.ESCAPE_CHAR):
k = k[1:]
2014-05-01 20:51:07 +02:00
for c in self.VALUE_ESCAPE:
v = v.replace(self.ESCAPE_CHAR + c, c)
try:
self.set('conf', secname, k, v)
except ValidationError as e:
e.section = secname
e.option = k
raise
2014-04-09 17:57:00 +02:00
def has_option(self, section, option):
2014-04-10 12:37:49 +02:00
"""Check if option exists in section.
2014-04-17 17:44:27 +02:00
Args:
2014-04-10 12:37:49 +02:00
section: The section name.
option: The option name
Return:
True if the option and section exist, False otherwise.
2014-04-10 12:24:41 +02:00
"""
2014-04-17 14:49:38 +02:00
if section not in self.sections:
return False
2014-04-17 14:49:38 +02:00
return option in self.sections[section]
2014-04-07 17:53:57 +02:00
2014-04-10 12:37:49 +02:00
def remove_option(self, section, option):
"""Remove an option.
2014-04-17 17:44:27 +02:00
Args:
2014-04-10 12:37:49 +02:00
section: The section where to remove an option.
option: The option name to remove.
Return:
True if the option existed, False otherwise.
"""
try:
2014-04-17 14:49:38 +02:00
sectdict = self.sections[section]
2014-04-10 12:37:49 +02:00
except KeyError:
raise NoSectionError(section)
option = self.optionxform(option)
existed = option in sectdict
if existed:
del sectdict[option]
return existed
2014-04-10 06:58:58 +02:00
@cmdutils.register(name='get', instance='config',
completion=['section', 'option'])
def get_wrapper(self, section, option):
2014-04-09 20:47:24 +02:00
"""Get the value from a section/option.
2014-04-17 17:44:27 +02:00
Wrapper for the get-command to output the value in the status bar.
2014-04-09 17:54:41 +02:00
"""
2014-04-17 19:11:31 +02:00
try:
2014-05-01 20:06:34 +02:00
val = self.get(section, option, transformed=False)
2014-04-17 19:11:31 +02:00
except (NoOptionError, NoSectionError) as e:
message.error("get: {} - {}".format(e.__class__.__name__, e))
else:
message.info("{} {} = {}".format(section, option, val))
2014-04-09 17:54:41 +02:00
2014-05-01 20:06:34 +02:00
def get(self, section, option, raw=False, transformed=True):
2014-03-28 07:18:40 +01:00
"""Get the value from a section/option.
2014-04-17 17:44:27 +02:00
Args:
2014-03-28 07:18:40 +01:00
section: The section to get the option from.
option: 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
"""
logging.debug("getting {} -> {}".format(section, option))
2014-02-26 07:44:39 +01:00
try:
2014-04-17 14:49:38 +02:00
sect = self.sections[section]
except KeyError:
2014-04-10 14:40:02 +02:00
raise NoSectionError(section)
try:
val = sect[option]
2014-02-26 07:44:39 +01:00
except KeyError:
2014-04-10 14:40:02 +02:00
raise NoOptionError(option, section)
if raw:
return val.value
mapping = {key: val.value for key, val in sect.values.items()}
newval = self._interpolation.before_get(self, section, option,
val.value, mapping)
logging.debug("interpolated val: {}".format(newval))
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
2014-04-17 12:06:48 +02:00
@cmdutils.register(name='set', instance='config', maxsplit=2,
2014-04-14 17:54:11 +02:00
completion=['section', 'option', 'value'])
def set_wrapper(self, section, option, value):
"""Set an option.
Wrapper for self.set() to output exceptions in the status bar.
"""
try:
2014-04-11 19:34:46 +02:00
self.set('conf', section, option, value)
except (NoOptionError, NoSectionError, ValidationError) as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
2014-04-17 12:06:48 +02:00
@cmdutils.register(name='set_temp', instance='config', maxsplit=2,
2014-04-14 17:54:11 +02:00
completion=['section', 'option', 'value'])
2014-04-11 19:34:46 +02:00
def set_temp_wrapper(self, section, option, value):
"""Set a temporary option.
Wrapper for self.set() to output exceptions in the status bar.
"""
try:
self.set('temp', section, option, value)
except (NoOptionError, NoSectionError, ValidationError) as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
def set(self, layer, section, option, value):
"""Set an option.
Args:
2014-04-17 17:44:27 +02:00
layer: A layer name as string (conf/temp/default).
section: The name of the section to change.
option: The name of the option to change.
value: The new value.
Raise:
NoSectionError: If the specified section doesn't exist.
NoOptionError: If the specified option doesn't exist.
Emit:
changed: If the config was changed.
style_changed: When style caches need to be invalidated.
"""
value = self._interpolation.before_set(self, section, option, value)
2014-04-10 07:09:12 +02:00
try:
2014-04-17 14:49:38 +02:00
sect = self.sections[section]
2014-04-10 07:09:12 +02:00
except KeyError:
raise NoSectionError(section)
mapping = {key: val.value for key, val in sect.values.items()}
interpolated = self._interpolation.before_get(self, section, option,
value, mapping)
2014-04-10 12:03:42 +02:00
try:
sect.setv(layer, option, value, interpolated)
2014-04-10 12:03:42 +02:00
except KeyError:
raise NoOptionError(option, section)
else:
if section in ['colors', 'fonts']:
self.style_changed.emit(section, option)
self.changed.emit(section, option)
2014-04-09 22:44:07 +02:00
2014-04-15 17:28:14 +02:00
@cmdutils.register(instance='config')
2014-02-26 07:44:39 +01:00
def save(self):
2014-02-26 09:18:27 +01:00
"""Save the config file."""
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
logging.debug("Saving config to {}".format(self._configfile))
with open(self._configfile, 'w') 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-04-17 14:49:38 +02:00
for secname, section in self.sections.items():
2014-04-17 12:35:46 +02:00
changed = section.dump_userconfig()
if changed:
2014-04-10 07:37:13 +02:00
lines.append('[{}]'.format(secname))
2014-04-17 12:35:46 +02:00
lines += ['{} = {}'.format(k, v) for k, v in changed]
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-04-10 12:37:49 +02:00
class SectionProxy(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-07 17:20:14 +02:00
# pylint: disable=redefined-builtin
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
def __repr__(self):
return '<Section: {}>'.format(self._name)
2014-03-27 22:37:34 +01:00
def __getitem__(self, key):
2014-04-10 12:37:49 +02:00
if not self._conf.has_option(self._name, key):
raise KeyError(key)
return self._conf.get(self._name, key)
2014-03-27 22:37:34 +01:00
def __setitem__(self, key, value):
2014-04-11 19:34:46 +02:00
return self._conf.set('conf', self._name, key, value)
2014-03-27 22:37:34 +01:00
def __delitem__(self, key):
2014-04-10 12:37:49 +02:00
if not (self._conf.has_option(self._name, key) and
self._conf.remove_option(self._name, key)):
raise KeyError(key)
2014-03-27 22:37:34 +01:00
def __contains__(self, key):
2014-04-10 12:37:49 +02:00
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."""
2014-04-17 14:49:38 +02:00
return self._conf.sections[self._name].keys()
2014-03-27 22:37:34 +01:00
2014-04-10 14:40:02 +02:00
def get(self, option, *, raw=False):
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-04-10 12:37:49 +02:00
option: The option name to get.
raw: Whether to get a raw value or not.
"""
2014-04-10 14:40:02 +02:00
# pylint: disable=arguments-differ
return self._conf.get(self._name, option, raw=raw)
2014-03-27 22:37:34 +01:00
@property
2014-04-10 12:37:49 +02:00
def conf(self):
"""The conf object of the proxy is read-only."""
return self._conf
2014-03-27 22:37:34 +01:00
@property
def name(self):
2014-04-10 12:37:49 +02:00
"""The name of the section on a proxy is read-only."""
2014-03-27 22:37:34 +01:00
return self._name