qutebrowser/qutebrowser/config/configfiles.py
2017-09-15 14:08:37 +02:00

224 lines
7.5 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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/>.
"""Configuration files residing on disk."""
import types
import os.path
import textwrap
import traceback
import configparser
import contextlib
from PyQt5.QtCore import QSettings
from qutebrowser.config import configexc
from qutebrowser.utils import objreg, standarddir, utils, qtutils
class StateConfig(configparser.ConfigParser):
"""The "state" file saving various application state."""
def __init__(self):
super().__init__()
save_manager = objreg.get('save-manager')
self._filename = os.path.join(standarddir.data(), 'state')
self.read(self._filename, encoding='utf-8')
for sect in ['general', 'geometry']:
try:
self.add_section(sect)
except configparser.DuplicateSectionError:
pass
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
self['general'].pop('fooled', None)
save_manager.add_saveable('state-config', self._save)
def _save(self):
"""Save the state file to the configured location."""
with open(self._filename, 'w', encoding='utf-8') as f:
self.write(f)
class YamlConfig:
"""A config stored on disk as YAML file.
Class attributes:
VERSION: The current version number of the config file.
"""
VERSION = 1
def __init__(self):
save_manager = objreg.get('save-manager')
self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml')
save_manager.add_saveable('yaml-config', self._save)
self.values = {}
def _save(self):
"""Save the changed settings to the YAML file."""
data = {'config_version': self.VERSION, 'global': self.values}
with qtutils.savefile_open(self._filename) as f:
f.write(textwrap.dedent("""
# DO NOT edit this file by hand, qutebrowser will overwrite it.
# Instead, create a config.py - see :help for details.
""".lstrip('\n')))
utils.yaml_dump(data, f)
def load(self):
"""Load self.values from the configured YAML file."""
# FIXME:conf error handling
try:
with open(self._filename, 'r', encoding='utf-8') as f:
self.values = utils.yaml_load(f)['global']
except FileNotFoundError:
pass
class ConfigAPI:
"""Object which gets passed to config.py as "config" object.
This is a small wrapper over the Config object, but with more
straightforward method names (get/set call get_obj/set_obj) and a more
shallow API.
Attributes:
_config: The main Config object to use.
_keyconfig: The KeyConfig object.
val: A matching ConfigContainer object.
load_autoconfig: Whether autoconfig.yml should be loaded.
errors: Errors which occurred while setting options.
"""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
self.load_autoconfig = True
self.errors = []
self.val = None # Set when initialized
@contextlib.contextmanager
def _handle_error(self, action, name):
try:
yield
except configexc.Error as e:
text = "While {} '{}'".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e))
def finalize(self):
"""Do work which needs to be done after reading config.py."""
self._config.update_mutables()
def get(self, name):
with self._handle_error('getting', name):
return self._config.get_obj(name)
def set(self, name, value):
with self._handle_error('setting', name):
self._config.set_obj(name, value)
def bind(self, key, command, *, mode, force=False):
with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode, force=force)
def unbind(self, key, *, mode):
with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode)
def read_config_py(filename=None):
"""Read a config.py file."""
from qutebrowser.config import config
api = ConfigAPI(config.instance, config.key_instance)
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
if not os.path.exists(filename):
return api
container = config.ConfigContainer(config.instance, configapi=api)
basename = os.path.basename(filename)
module = types.ModuleType('config')
module.config = api
module.c = container
module.__file__ = filename
try:
with open(filename, mode='rb') as f:
source = f.read()
except OSError as e:
text = "Error while reading {}".format(basename)
desc = configexc.ConfigErrorDesc(text, e)
raise configexc.ConfigFileErrors(basename, [desc])
try:
code = compile(source, filename, 'exec')
except ValueError as e:
# source contains NUL bytes
desc = configexc.ConfigErrorDesc("Error while compiling", e)
raise configexc.ConfigFileErrors(basename, [desc])
except SyntaxError as e:
desc = configexc.ConfigErrorDesc("Syntax Error", e,
traceback=traceback.format_exc())
raise configexc.ConfigFileErrors(basename, [desc])
try:
exec(code, module.__dict__)
except Exception as e:
api.errors.append(configexc.ConfigErrorDesc(
"Unhandled exception",
exception=e, traceback=traceback.format_exc()))
api.finalize()
return api
def init(config):
"""Initialize config storage not related to the main config."""
state = StateConfig()
objreg.register('state-config', state)
# We need to import this here because lineparser needs config.
# FIXME:conf add this to the Command widget or something?
from qutebrowser.misc import lineparser
save_manager = objreg.get('save-manager')
command_history = lineparser.LimitLineParser(
standarddir.data(), 'cmd-history',
limit='completion.cmd_history_max_items',
parent=config)
objreg.register('command-history', command_history)
save_manager.add_saveable('command-history', command_history.save,
command_history.changed)
# 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/qutebrowser/qutebrowser/issues/515
path = os.path.join(standarddir.config(auto=True), 'qsettings')
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
QSettings.setPath(fmt, QSettings.UserScope, path)