Initial attempt at error handling for config.py

This commit is contained in:
Florian Bruhin 2017-09-14 21:51:29 +02:00
parent 5a11c96e56
commit 490de32b49
6 changed files with 137 additions and 23 deletions

View File

@ -408,7 +408,7 @@ def _init_modules(args, crash_handler):
objreg.register('readline-bridge', readline_bridge)
log.init.debug("Initializing config...")
config.init(qApp)
config.init(args, qApp)
save_manager.init_autosave()
log.init.debug("Initializing sql...")

View File

@ -26,7 +26,7 @@ import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from qutebrowser.config import configdata, configexc, configtypes, configfiles
from qutebrowser.utils import utils, objreg, message, log, usertypes
from qutebrowser.utils import utils, objreg, message, log, usertypes, error
from qutebrowser.misc import objects
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.completion.models import configmodel
@ -500,18 +500,28 @@ class ConfigContainer:
Attributes:
_config: The Config object.
_prefix: The __getattr__ chain leading up to this object.
_confpy: If True, get values suitable for config.py.
_confpy: If True, get values suitable for config.py and
do not raise exceptions.
_errors: If confpy=True is given, a list of configexc.ConfigErrorDesc.
"""
def __init__(self, config, confpy=False, prefix=''):
self._config = config
self._prefix = prefix
self._confpy = confpy
self._errors = []
def __repr__(self):
return utils.get_repr(self, constructor=True, config=self._config,
confpy=self._confpy, prefix=self._prefix)
@contextlib.contextmanager
def _handle_error(self, action, name):
try:
yield
except configexc.Error as e:
self._errors.append(configexc.ConfigErrorDesc(action, name, e))
def __getattr__(self, attr):
"""Get an option or a new ConfigContainer with the added prefix.
@ -529,16 +539,20 @@ class ConfigContainer:
return ConfigContainer(config=self._config, confpy=self._confpy,
prefix=name)
if self._confpy:
return self._config.get_obj(name)
else:
return self._config.get(name)
with self._handle_error('getting', name):
if self._confpy:
return self._config.get_obj(name)
else:
return self._config.get(name)
def __setattr__(self, attr, value):
"""Set the given option in the config."""
if attr.startswith('_'):
return super().__setattr__(attr, value)
self._config.set_obj(self._join(attr), value)
name = self._join(attr)
with self._handle_error('setting', name):
self._config.set_obj(name, value)
def _join(self, attr):
"""Get the prefix joined with the given attribute."""
@ -616,7 +630,7 @@ class StyleSheetObserver(QObject):
instance.changed.connect(self._update_stylesheet)
def init(parent=None):
def init(args, parent=None):
"""Initialize the config.
Args:
@ -639,7 +653,13 @@ def init(parent=None):
config_commands = ConfigCommands(instance, key_instance)
objreg.register('config-commands', config_commands)
config_api = configfiles.read_config_py()
try:
config_api = configfiles.read_config_py()
except configexc.ConfigFileError as e:
error.handle_fatal_exc(e, args=args,
title="Error while loading config")
config_api = None
if getattr(config_api, 'load_autoconfig', True):
instance.read_yaml()

View File

@ -20,6 +20,9 @@
"""Exceptions related to config parsing."""
import traceback
class Error(Exception):
"""Base exception for config-related errors."""
@ -70,3 +73,51 @@ class NoOptionError(Error):
def __init__(self, option):
super().__init__("No option {!r}".format(option))
self.option = option
class ConfigFileError(Error):
"""Raised when there was an error while loading a config file."""
def __init__(self, basename, msg):
super().__init__("Failed to load {}: {}".format(basename, msg))
class ConfigFileUnhandledException(ConfigFileError):
"""Raised when there was an unhandled exception while loading config.py.
Needs to be raised from an exception handler.
"""
def __init__(self, basename):
super().__init__(basename, "Unhandled exception\n\n{}".format(
traceback.format_exc()))
class ConfigErrorDesc:
"""A description of an error happening while reading the config.
Attributes:
_action: What action has been taken, e.g 'set'
_name: The option which was set, or the key which was bound.
_exception: The exception which happened.
"""
def __init__(self, action, name, exception):
self._action = action
self._exception = exception
self._name = name
def __str__(self):
return "While {} {}: {}".format(
self._action, self._name, self._exception)
class ConfigFileErrors(ConfigFileError):
"""Raised when multiple errors occurred inside the config."""
def __init__(self, basename, errors):
super().__init__(basename, "\n\n".join(str(err) for err in errors))

View File

@ -22,10 +22,13 @@
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
@ -104,6 +107,7 @@ class ConfigAPI:
_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, container):
@ -111,24 +115,41 @@ class ConfigAPI:
self._keyconfig = keyconfig
self.val = container
self.load_autoconfig = True
self.errors = []
@contextlib.contextmanager
def _handle_error(self, action, name):
try:
yield
except configexc.Error as e:
self.errors.append(configexc.ConfigErrorDesc(action, name, e))
def finalize(self):
"""Needs to get called after reading config.py is done."""
self._config.update_mutables()
self.errors += self.val._errors # pylint: disable=protected-access
def get(self, name):
return self._config.get_obj(name)
with self._handle_error('getting', name):
return self._config.get_obj(name)
def set(self, name, value):
self._config.set_obj(name, value)
with self._handle_error('setting', name):
self._config.set_obj(name, value)
def bind(self, key, command, *, mode, force=False):
self._keyconfig.bind(key, command, mode=mode, force=force)
with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode, force=force)
def unbind(self, key, *, mode):
self._keyconfig.unbind(key, mode=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
# FIXME:conf error handling
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
if not os.path.exists(filename):
@ -140,13 +161,30 @@ def read_config_py(filename=None):
module.config = api
module.c = api.val
module.__file__ = filename
basename = os.path.basename(filename)
with open(filename, mode='rb') as f:
source = f.read()
code = compile(source, filename, 'exec')
exec(code, module.__dict__)
try:
with open(filename, mode='rb') as f:
source = f.read()
except OSError as e:
raise configexc.ConfigFileError(basename, e.strerror)
config.instance.update_mutables()
try:
code = compile(source, filename, 'exec')
except ValueError as e:
# source contains NUL bytes
raise configexc.ConfigFileError(basename, str(e))
except SyntaxError:
raise configexc.ConfigFileUnhandledException(basename)
try:
exec(code, module.__dict__)
except Exception:
raise configexc.ConfigFileUnhandledException(basename)
api.finalize()
if api.errors:
raise configexc.ConfigFileErrors(basename, api.errors)
return api

View File

@ -19,6 +19,7 @@
"""Tools related to error printing/displaying."""
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.utils import log, utils
@ -35,7 +36,8 @@ def _get_name(exc):
return name
def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
def handle_fatal_exc(exc, args, title, *, pre_text='', post_text='',
richtext=False):
"""Handle a fatal "expected" exception by displaying an error box.
If --no-err-windows is given as argument, the text is logged to the error
@ -47,6 +49,7 @@ def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
title: The title to be used for the error message.
pre_text: The text to be displayed before the exception text.
post_text: The text to be displayed after the exception text.
richtext: If given, interpret the given text as rich text.
"""
if args.no_err_windows:
lines = [
@ -66,4 +69,6 @@ def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
if post_text:
msg_text += '\n\n{}'.format(post_text)
msgbox = QMessageBox(QMessageBox.Critical, title, msg_text)
if richtext:
msgbox.setTextFormat(Qt.RichText)
msgbox.exec_()

View File

@ -877,7 +877,7 @@ def test_init(init_patch, fake_save_manager, config_tmpdir, load_autoconfig):
config_py_lines.append('config.load_autoconfig = False')
config_py_file.write_text('\n'.join(config_py_lines), 'utf-8', ensure=True)
config.init()
config.init(args=None)
objreg.get('config-commands')
assert isinstance(config.instance, config.Config)
@ -899,4 +899,4 @@ def test_init(init_patch, fake_save_manager, config_tmpdir, load_autoconfig):
def test_init_invalid_change_filter(init_patch):
config.change_filter('foobar')
with pytest.raises(configexc.NoOptionError):
config.init()
config.init(args=None)