Initial attempt at error handling for config.py
This commit is contained in:
parent
5a11c96e56
commit
490de32b49
@ -408,7 +408,7 @@ def _init_modules(args, crash_handler):
|
|||||||
objreg.register('readline-bridge', readline_bridge)
|
objreg.register('readline-bridge', readline_bridge)
|
||||||
|
|
||||||
log.init.debug("Initializing config...")
|
log.init.debug("Initializing config...")
|
||||||
config.init(qApp)
|
config.init(args, qApp)
|
||||||
save_manager.init_autosave()
|
save_manager.init_autosave()
|
||||||
|
|
||||||
log.init.debug("Initializing sql...")
|
log.init.debug("Initializing sql...")
|
||||||
|
@ -26,7 +26,7 @@ import functools
|
|||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||||
|
|
||||||
from qutebrowser.config import configdata, configexc, configtypes, configfiles
|
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.misc import objects
|
||||||
from qutebrowser.commands import cmdexc, cmdutils
|
from qutebrowser.commands import cmdexc, cmdutils
|
||||||
from qutebrowser.completion.models import configmodel
|
from qutebrowser.completion.models import configmodel
|
||||||
@ -500,18 +500,28 @@ class ConfigContainer:
|
|||||||
Attributes:
|
Attributes:
|
||||||
_config: The Config object.
|
_config: The Config object.
|
||||||
_prefix: The __getattr__ chain leading up to this 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=''):
|
def __init__(self, config, confpy=False, prefix=''):
|
||||||
self._config = config
|
self._config = config
|
||||||
self._prefix = prefix
|
self._prefix = prefix
|
||||||
self._confpy = confpy
|
self._confpy = confpy
|
||||||
|
self._errors = []
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, constructor=True, config=self._config,
|
return utils.get_repr(self, constructor=True, config=self._config,
|
||||||
confpy=self._confpy, prefix=self._prefix)
|
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):
|
def __getattr__(self, attr):
|
||||||
"""Get an option or a new ConfigContainer with the added prefix.
|
"""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,
|
return ConfigContainer(config=self._config, confpy=self._confpy,
|
||||||
prefix=name)
|
prefix=name)
|
||||||
|
|
||||||
if self._confpy:
|
with self._handle_error('getting', name):
|
||||||
return self._config.get_obj(name)
|
if self._confpy:
|
||||||
else:
|
return self._config.get_obj(name)
|
||||||
return self._config.get(name)
|
else:
|
||||||
|
return self._config.get(name)
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
"""Set the given option in the config."""
|
"""Set the given option in the config."""
|
||||||
if attr.startswith('_'):
|
if attr.startswith('_'):
|
||||||
return super().__setattr__(attr, value)
|
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):
|
def _join(self, attr):
|
||||||
"""Get the prefix joined with the given attribute."""
|
"""Get the prefix joined with the given attribute."""
|
||||||
@ -616,7 +630,7 @@ class StyleSheetObserver(QObject):
|
|||||||
instance.changed.connect(self._update_stylesheet)
|
instance.changed.connect(self._update_stylesheet)
|
||||||
|
|
||||||
|
|
||||||
def init(parent=None):
|
def init(args, parent=None):
|
||||||
"""Initialize the config.
|
"""Initialize the config.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -639,7 +653,13 @@ def init(parent=None):
|
|||||||
config_commands = ConfigCommands(instance, key_instance)
|
config_commands = ConfigCommands(instance, key_instance)
|
||||||
objreg.register('config-commands', config_commands)
|
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):
|
if getattr(config_api, 'load_autoconfig', True):
|
||||||
instance.read_yaml()
|
instance.read_yaml()
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@
|
|||||||
"""Exceptions related to config parsing."""
|
"""Exceptions related to config parsing."""
|
||||||
|
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
|
|
||||||
"""Base exception for config-related errors."""
|
"""Base exception for config-related errors."""
|
||||||
@ -70,3 +73,51 @@ class NoOptionError(Error):
|
|||||||
def __init__(self, option):
|
def __init__(self, option):
|
||||||
super().__init__("No option {!r}".format(option))
|
super().__init__("No option {!r}".format(option))
|
||||||
self.option = 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))
|
||||||
|
@ -22,10 +22,13 @@
|
|||||||
import types
|
import types
|
||||||
import os.path
|
import os.path
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import traceback
|
||||||
import configparser
|
import configparser
|
||||||
|
import contextlib
|
||||||
|
|
||||||
from PyQt5.QtCore import QSettings
|
from PyQt5.QtCore import QSettings
|
||||||
|
|
||||||
|
from qutebrowser.config import configexc
|
||||||
from qutebrowser.utils import objreg, standarddir, utils, qtutils
|
from qutebrowser.utils import objreg, standarddir, utils, qtutils
|
||||||
|
|
||||||
|
|
||||||
@ -104,6 +107,7 @@ class ConfigAPI:
|
|||||||
_keyconfig: The KeyConfig object.
|
_keyconfig: The KeyConfig object.
|
||||||
val: A matching ConfigContainer object.
|
val: A matching ConfigContainer object.
|
||||||
load_autoconfig: Whether autoconfig.yml should be loaded.
|
load_autoconfig: Whether autoconfig.yml should be loaded.
|
||||||
|
errors: Errors which occurred while setting options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config, keyconfig, container):
|
def __init__(self, config, keyconfig, container):
|
||||||
@ -111,24 +115,41 @@ class ConfigAPI:
|
|||||||
self._keyconfig = keyconfig
|
self._keyconfig = keyconfig
|
||||||
self.val = container
|
self.val = container
|
||||||
self.load_autoconfig = True
|
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):
|
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):
|
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):
|
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):
|
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):
|
def read_config_py(filename=None):
|
||||||
"""Read a config.py file."""
|
"""Read a config.py file."""
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
# FIXME:conf error handling
|
|
||||||
if filename is None:
|
if filename is None:
|
||||||
filename = os.path.join(standarddir.config(), 'config.py')
|
filename = os.path.join(standarddir.config(), 'config.py')
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
@ -140,13 +161,30 @@ def read_config_py(filename=None):
|
|||||||
module.config = api
|
module.config = api
|
||||||
module.c = api.val
|
module.c = api.val
|
||||||
module.__file__ = filename
|
module.__file__ = filename
|
||||||
|
basename = os.path.basename(filename)
|
||||||
|
|
||||||
with open(filename, mode='rb') as f:
|
try:
|
||||||
source = f.read()
|
with open(filename, mode='rb') as f:
|
||||||
code = compile(source, filename, 'exec')
|
source = f.read()
|
||||||
exec(code, module.__dict__)
|
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
|
return api
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
"""Tools related to error printing/displaying."""
|
"""Tools related to error printing/displaying."""
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from qutebrowser.utils import log, utils
|
from qutebrowser.utils import log, utils
|
||||||
@ -35,7 +36,8 @@ def _get_name(exc):
|
|||||||
return name
|
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.
|
"""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
|
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.
|
title: The title to be used for the error message.
|
||||||
pre_text: The text to be displayed before the exception text.
|
pre_text: The text to be displayed before the exception text.
|
||||||
post_text: The text to be displayed after 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:
|
if args.no_err_windows:
|
||||||
lines = [
|
lines = [
|
||||||
@ -66,4 +69,6 @@ def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
|
|||||||
if post_text:
|
if post_text:
|
||||||
msg_text += '\n\n{}'.format(post_text)
|
msg_text += '\n\n{}'.format(post_text)
|
||||||
msgbox = QMessageBox(QMessageBox.Critical, title, msg_text)
|
msgbox = QMessageBox(QMessageBox.Critical, title, msg_text)
|
||||||
|
if richtext:
|
||||||
|
msgbox.setTextFormat(Qt.RichText)
|
||||||
msgbox.exec_()
|
msgbox.exec_()
|
||||||
|
@ -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_lines.append('config.load_autoconfig = False')
|
||||||
config_py_file.write_text('\n'.join(config_py_lines), 'utf-8', ensure=True)
|
config_py_file.write_text('\n'.join(config_py_lines), 'utf-8', ensure=True)
|
||||||
|
|
||||||
config.init()
|
config.init(args=None)
|
||||||
|
|
||||||
objreg.get('config-commands')
|
objreg.get('config-commands')
|
||||||
assert isinstance(config.instance, config.Config)
|
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):
|
def test_init_invalid_change_filter(init_patch):
|
||||||
config.change_filter('foobar')
|
config.change_filter('foobar')
|
||||||
with pytest.raises(configexc.NoOptionError):
|
with pytest.raises(configexc.NoOptionError):
|
||||||
config.init()
|
config.init(args=None)
|
||||||
|
Loading…
Reference in New Issue
Block a user