2014-06-19 09:04:37 +02:00
|
|
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
|
2018-02-05 12:19:50 +01:00
|
|
|
# Copyright 2014-2018 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/>.
|
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
"""Configuration storage and config-related utilities."""
|
2014-03-27 22:37:34 +01:00
|
|
|
|
2017-06-20 14:55:33 +02:00
|
|
|
import copy
|
2017-06-16 15:05:36 +02:00
|
|
|
import contextlib
|
2014-08-27 20:16:04 +02:00
|
|
|
import functools
|
2014-01-27 21:42:00 +01:00
|
|
|
|
2017-10-02 07:06:05 +02:00
|
|
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
2014-04-10 18:01:16 +02:00
|
|
|
|
2018-02-16 11:18:43 +01:00
|
|
|
from qutebrowser.config import configdata, configexc, configutils
|
2017-10-02 07:06:05 +02:00
|
|
|
from qutebrowser.utils import utils, log, jinja
|
2017-09-22 14:08:06 +02:00
|
|
|
from qutebrowser.misc import objects
|
2017-12-29 14:22:20 +01:00
|
|
|
from qutebrowser.keyinput import keyutils
|
2015-09-29 07:35:38 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
# An easy way to access the config from other code via config.val.foo
|
2017-06-12 13:19:56 +02:00
|
|
|
val = None
|
2017-06-13 13:47:06 +02:00
|
|
|
instance = None
|
2017-06-16 13:28:51 +02:00
|
|
|
key_instance = None
|
2014-09-28 11:27:52 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
# Keeping track of all change filters to validate them later.
|
2017-09-22 14:08:06 +02:00
|
|
|
change_filters = []
|
2014-09-28 11:27:52 +02:00
|
|
|
|
2018-02-15 22:54:22 +01:00
|
|
|
# Sentinel
|
|
|
|
UNSET = object()
|
|
|
|
|
2017-06-14 21:42:36 +02:00
|
|
|
|
2017-11-24 16:25:01 +01:00
|
|
|
class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
2015-02-14 00:25:26 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
"""Decorator to filter calls based on a config section/option matching.
|
2015-02-14 00:25:26 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
This could also be a function, but as a class (with a "wrong" name) it's
|
|
|
|
much cleaner to implement.
|
2015-04-06 00:10:37 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
Attributes:
|
|
|
|
_option: An option or prefix to be filtered
|
|
|
|
_function: Whether a function rather than a method is decorated.
|
2015-04-06 00:10:37 +02:00
|
|
|
"""
|
2014-10-02 21:58:34 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
def __init__(self, option, function=False):
|
|
|
|
"""Save decorator arguments.
|
2015-02-14 00:25:26 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
Gets called on parse-time with the decorator arguments.
|
2015-01-31 22:56:23 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
Args:
|
|
|
|
option: The option to be filtered.
|
|
|
|
function: Whether a function rather than a method is decorated.
|
|
|
|
"""
|
|
|
|
self._option = option
|
|
|
|
self._function = function
|
2017-09-22 14:08:06 +02:00
|
|
|
change_filters.append(self)
|
2014-09-28 00:27:22 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
def validate(self):
|
|
|
|
"""Make sure the configured option or prefix exists.
|
2015-02-18 22:07:01 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
We can't do this in __init__ as configdata isn't ready yet.
|
|
|
|
"""
|
|
|
|
if (self._option not in configdata.DATA and
|
|
|
|
not configdata.is_valid_prefix(self._option)):
|
|
|
|
raise configexc.NoOptionError(self._option)
|
|
|
|
|
|
|
|
def _check_match(self, option):
|
|
|
|
"""Check if the given option matches the filter."""
|
|
|
|
if option is None:
|
|
|
|
# Called directly, not from a config change event.
|
|
|
|
return True
|
|
|
|
elif option == self._option:
|
|
|
|
return True
|
|
|
|
elif option.startswith(self._option + '.'):
|
|
|
|
# prefix match
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
2015-02-18 22:07:01 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
def __call__(self, func):
|
|
|
|
"""Filter calls to the decorated function.
|
2014-09-28 00:27:22 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
Gets called when a function should be decorated.
|
2015-02-14 00:25:26 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
Adds a filter which returns if we're not interested in the change-event
|
|
|
|
and calls the wrapped function if we are.
|
2015-02-14 00:25:26 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
We assume the function passed doesn't take any parameters.
|
2015-06-08 19:34:11 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
Args:
|
|
|
|
func: The function to be decorated.
|
2015-06-08 19:34:11 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
Return:
|
|
|
|
The decorated function.
|
|
|
|
"""
|
|
|
|
if self._function:
|
|
|
|
@functools.wraps(func)
|
|
|
|
def wrapper(option=None):
|
2017-12-15 13:55:06 +01:00
|
|
|
"""Call the underlying function."""
|
2017-06-16 15:05:36 +02:00
|
|
|
if self._check_match(option):
|
|
|
|
return func()
|
2017-12-15 16:05:20 +01:00
|
|
|
return None
|
2016-08-16 17:44:48 +02:00
|
|
|
else:
|
2017-06-16 15:05:36 +02:00
|
|
|
@functools.wraps(func)
|
|
|
|
def wrapper(wrapper_self, option=None):
|
2017-12-15 13:55:06 +01:00
|
|
|
"""Call the underlying function."""
|
2017-06-16 15:05:36 +02:00
|
|
|
if self._check_match(option):
|
|
|
|
return func(wrapper_self)
|
2017-12-15 16:05:20 +01:00
|
|
|
return None
|
2017-06-16 15:05:36 +02:00
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
2017-06-20 17:13:46 +02:00
|
|
|
class KeyConfig:
|
2017-06-16 15:05:36 +02:00
|
|
|
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Utilities related to keybindings.
|
|
|
|
|
|
|
|
Note that the actual values are saved in the config itself, not here.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
_config: The Config object to be used.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
self._config = config
|
2017-06-19 16:41:17 +02:00
|
|
|
|
2018-02-27 14:16:41 +01:00
|
|
|
def _validate(self, key, mode):
|
|
|
|
"""Validate the given key and mode."""
|
2017-12-29 15:58:20 +01:00
|
|
|
# Catch old usage of this code
|
|
|
|
assert isinstance(key, keyutils.KeySequence), key
|
2017-07-02 22:10:28 +02:00
|
|
|
if mode not in configdata.DATA['bindings.default'].default:
|
|
|
|
raise configexc.KeybindingError("Invalid mode {}!".format(mode))
|
|
|
|
|
2017-06-30 16:26:26 +02:00
|
|
|
def get_bindings_for(self, mode):
|
|
|
|
"""Get the combined bindings for the given mode."""
|
2017-12-29 15:41:28 +01:00
|
|
|
bindings = dict(val.bindings.default[mode])
|
|
|
|
for key, binding in val.bindings.commands[mode].items():
|
2018-03-08 11:41:31 +01:00
|
|
|
if not binding:
|
2017-07-02 17:12:31 +02:00
|
|
|
bindings.pop(key, None)
|
2017-06-30 18:54:17 +02:00
|
|
|
else:
|
|
|
|
bindings[key] = binding
|
2017-06-30 16:26:26 +02:00
|
|
|
return bindings
|
|
|
|
|
2017-06-19 16:41:17 +02:00
|
|
|
def get_reverse_bindings_for(self, mode):
|
|
|
|
"""Get a dict of commands to a list of bindings for the mode."""
|
2017-06-16 15:05:36 +02:00
|
|
|
cmd_to_keys = {}
|
2017-06-30 16:26:26 +02:00
|
|
|
bindings = self.get_bindings_for(mode)
|
2018-02-27 06:56:57 +01:00
|
|
|
for seq, full_cmd in sorted(bindings.items()):
|
2017-06-16 15:05:36 +02:00
|
|
|
for cmd in full_cmd.split(';;'):
|
|
|
|
cmd = cmd.strip()
|
|
|
|
cmd_to_keys.setdefault(cmd, [])
|
2018-02-27 06:56:57 +01:00
|
|
|
# Put bindings involving modifiers last
|
|
|
|
if any(info.modifiers for info in seq):
|
|
|
|
cmd_to_keys[cmd].append(str(seq))
|
|
|
|
else:
|
|
|
|
cmd_to_keys[cmd].insert(0, str(seq))
|
2017-06-16 15:05:36 +02:00
|
|
|
return cmd_to_keys
|
2014-04-25 15:57:28 +02:00
|
|
|
|
2017-11-26 12:34:18 +01:00
|
|
|
def get_command(self, key, mode, default=False):
|
2017-07-02 22:10:28 +02:00
|
|
|
"""Get the command for a given key (or None)."""
|
2018-02-27 14:16:41 +01:00
|
|
|
self._validate(key, mode)
|
2017-11-26 12:34:18 +01:00
|
|
|
if default:
|
2017-12-29 15:41:28 +01:00
|
|
|
bindings = dict(val.bindings.default[mode])
|
2017-11-26 12:34:18 +01:00
|
|
|
else:
|
|
|
|
bindings = self.get_bindings_for(mode)
|
2017-07-02 22:10:28 +02:00
|
|
|
return bindings.get(key, None)
|
2017-06-19 16:41:17 +02:00
|
|
|
|
2017-10-03 19:03:03 +02:00
|
|
|
def bind(self, key, command, *, mode, save_yaml=False):
|
2017-06-19 16:41:17 +02:00
|
|
|
"""Add a new binding from key to command."""
|
2017-11-08 15:08:36 +01:00
|
|
|
if command is not None and not command.strip():
|
|
|
|
raise configexc.KeybindingError(
|
|
|
|
"Can't add binding '{}' with empty command in {} "
|
|
|
|
'mode'.format(key, mode))
|
|
|
|
|
2018-02-27 14:16:41 +01:00
|
|
|
self._validate(key, mode)
|
2017-06-19 16:41:17 +02:00
|
|
|
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
|
|
|
|
key, command, mode))
|
2017-06-30 16:26:26 +02:00
|
|
|
|
2018-02-18 20:06:43 +01:00
|
|
|
bindings = self._config.get_mutable_obj('bindings.commands')
|
2017-07-01 15:51:05 +02:00
|
|
|
if mode not in bindings:
|
|
|
|
bindings[mode] = {}
|
2017-12-29 15:58:20 +01:00
|
|
|
bindings[mode][str(key)] = command
|
2017-06-20 17:13:46 +02:00
|
|
|
self._config.update_mutables(save_yaml=save_yaml)
|
2017-06-19 16:41:17 +02:00
|
|
|
|
2017-10-08 21:48:48 +02:00
|
|
|
def bind_default(self, key, *, mode='normal', save_yaml=False):
|
2017-10-08 23:10:08 +02:00
|
|
|
"""Restore a default keybinding."""
|
2018-02-27 14:16:41 +01:00
|
|
|
self._validate(key, mode)
|
2017-10-08 21:48:48 +02:00
|
|
|
|
2018-02-18 20:06:43 +01:00
|
|
|
bindings_commands = self._config.get_mutable_obj('bindings.commands')
|
2017-10-08 21:48:48 +02:00
|
|
|
try:
|
2017-12-29 15:58:20 +01:00
|
|
|
del bindings_commands[mode][str(key)]
|
2017-10-08 21:48:48 +02:00
|
|
|
except KeyError:
|
|
|
|
raise configexc.KeybindingError(
|
|
|
|
"Can't find binding '{}' in {} mode".format(key, mode))
|
|
|
|
self._config.update_mutables(save_yaml=save_yaml)
|
|
|
|
|
2017-06-20 16:53:46 +02:00
|
|
|
def unbind(self, key, *, mode='normal', save_yaml=False):
|
2017-06-19 16:41:17 +02:00
|
|
|
"""Unbind the given key in the given mode."""
|
2018-02-27 14:16:41 +01:00
|
|
|
self._validate(key, mode)
|
2017-06-30 16:26:26 +02:00
|
|
|
|
2018-02-18 20:06:43 +01:00
|
|
|
bindings_commands = self._config.get_mutable_obj('bindings.commands')
|
2017-06-30 18:54:17 +02:00
|
|
|
|
2018-02-27 07:40:06 +01:00
|
|
|
if val.bindings.commands[mode].get(key, None) is not None:
|
2017-06-30 16:26:26 +02:00
|
|
|
# In custom bindings -> remove it
|
2017-12-29 15:58:20 +01:00
|
|
|
del bindings_commands[mode][str(key)]
|
2017-12-29 15:41:28 +01:00
|
|
|
elif key in val.bindings.default[mode]:
|
2017-06-30 18:54:17 +02:00
|
|
|
# In default bindings -> shadow it with None
|
2017-07-01 15:51:05 +02:00
|
|
|
if mode not in bindings_commands:
|
|
|
|
bindings_commands[mode] = {}
|
2017-12-29 15:58:20 +01:00
|
|
|
bindings_commands[mode][str(key)] = None
|
2017-06-30 16:26:26 +02:00
|
|
|
else:
|
2017-07-02 22:10:28 +02:00
|
|
|
raise configexc.KeybindingError(
|
|
|
|
"Can't find binding '{}' in {} mode".format(key, mode))
|
2017-06-30 16:26:26 +02:00
|
|
|
|
2017-06-20 17:13:46 +02:00
|
|
|
self._config.update_mutables(save_yaml=save_yaml)
|
2017-06-19 16:41:17 +02:00
|
|
|
|
2014-03-09 20:10:57 +01:00
|
|
|
|
2017-06-20 17:13:46 +02:00
|
|
|
class Config(QObject):
|
|
|
|
|
|
|
|
"""Main config object.
|
|
|
|
|
2018-02-18 19:11:12 +01:00
|
|
|
Class attributes:
|
2018-02-19 04:30:24 +01:00
|
|
|
MUTABLE_TYPES: Types returned from the config which could potentially
|
|
|
|
be mutated.
|
2018-02-18 19:11:12 +01:00
|
|
|
|
2017-06-20 17:13:46 +02:00
|
|
|
Attributes:
|
2018-02-16 11:18:43 +01:00
|
|
|
_values: A dict mapping setting names to configutils.Values objects.
|
2017-09-19 23:26:02 +02:00
|
|
|
_mutables: A dictionary of mutable objects to be checked for changes.
|
2017-07-02 21:07:38 +02:00
|
|
|
_yaml: A YamlConfig object or None.
|
2017-06-20 17:13:46 +02:00
|
|
|
|
|
|
|
Signals:
|
|
|
|
changed: Emitted with the option name when an option changed.
|
|
|
|
"""
|
2014-04-10 12:37:49 +02:00
|
|
|
|
2018-02-18 19:11:12 +01:00
|
|
|
MUTABLE_TYPES = (dict, list)
|
2017-06-20 16:23:23 +02:00
|
|
|
changed = pyqtSignal(str)
|
2014-04-07 17:53:57 +02:00
|
|
|
|
2017-07-02 21:07:38 +02:00
|
|
|
def __init__(self, yaml_config, parent=None):
|
2017-06-16 15:05:36 +02:00
|
|
|
super().__init__(parent)
|
2017-07-11 21:59:17 +02:00
|
|
|
self.changed.connect(_render_stylesheet.cache_clear)
|
2017-09-17 23:07:52 +02:00
|
|
|
self._mutables = {}
|
2017-07-02 21:07:38 +02:00
|
|
|
self._yaml = yaml_config
|
2018-02-19 19:16:50 +01:00
|
|
|
self._init_values()
|
2014-04-10 12:37:49 +02:00
|
|
|
|
2018-02-19 19:16:50 +01:00
|
|
|
def _init_values(self):
|
|
|
|
"""Populate the self._values dict."""
|
2018-02-18 20:06:43 +01:00
|
|
|
self._values = {}
|
|
|
|
for name, opt in configdata.DATA.items():
|
|
|
|
self._values[name] = configutils.Values(opt)
|
2014-04-10 12:37:49 +02:00
|
|
|
|
2017-10-04 11:14:24 +02:00
|
|
|
def __iter__(self):
|
2018-02-19 06:35:14 +01:00
|
|
|
"""Iterate over configutils.Values items."""
|
|
|
|
yield from self._values.values()
|
2017-10-04 11:14:24 +02:00
|
|
|
|
2017-09-17 20:06:35 +02:00
|
|
|
def init_save_manager(self, save_manager):
|
|
|
|
"""Make sure the config gets saved properly.
|
|
|
|
|
|
|
|
We do this outside of __init__ because the config gets created before
|
|
|
|
the save_manager exists.
|
|
|
|
"""
|
|
|
|
self._yaml.init_save_manager(save_manager)
|
|
|
|
|
2018-02-15 21:28:05 +01:00
|
|
|
def _set_value(self, opt, value, pattern=None):
|
2017-07-03 13:58:19 +02:00
|
|
|
"""Set the given option to the given value."""
|
2017-09-18 19:33:56 +02:00
|
|
|
if not isinstance(objects.backend, objects.NoBackend):
|
2017-07-03 13:58:19 +02:00
|
|
|
if objects.backend not in opt.backends:
|
2017-11-24 09:23:35 +01:00
|
|
|
raise configexc.BackendError(opt.name, objects.backend)
|
2017-07-03 13:58:19 +02:00
|
|
|
|
|
|
|
opt.typ.to_py(value) # for validation
|
2018-02-20 23:21:24 +01:00
|
|
|
|
2018-02-18 20:06:43 +01:00
|
|
|
self._values[opt.name].add(opt.typ.from_obj(value), pattern)
|
2017-07-03 13:58:19 +02:00
|
|
|
|
|
|
|
self.changed.emit(opt.name)
|
|
|
|
log.config.debug("Config option changed: {} = {}".format(
|
|
|
|
opt.name, value))
|
2017-06-19 16:41:17 +02:00
|
|
|
|
2018-03-07 18:30:44 +01:00
|
|
|
def _check_yaml(self, opt, save_yaml):
|
|
|
|
"""Make sure the given option may be set in autoconfig.yml."""
|
|
|
|
if save_yaml and opt.no_autoconfig:
|
|
|
|
raise configexc.NoAutoconfigError(opt.name)
|
|
|
|
|
2017-06-20 16:53:46 +02:00
|
|
|
def read_yaml(self):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Read the YAML settings from self._yaml."""
|
2017-06-20 16:53:46 +02:00
|
|
|
self._yaml.load()
|
2018-02-19 06:35:14 +01:00
|
|
|
for values in self._yaml:
|
|
|
|
for scoped in values:
|
|
|
|
self._set_value(values.opt, scoped.value,
|
|
|
|
pattern=scoped.pattern)
|
2017-06-20 16:53:46 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
def get_opt(self, name):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Get a configdata.Option object for the given setting."""
|
2014-04-10 12:37:49 +02:00
|
|
|
try:
|
2017-07-03 10:41:39 +02:00
|
|
|
return configdata.DATA[name]
|
2014-04-10 12:37:49 +02:00
|
|
|
except KeyError:
|
2017-10-11 08:00:38 +02:00
|
|
|
deleted = name in configdata.MIGRATIONS.deleted
|
|
|
|
renamed = configdata.MIGRATIONS.renamed.get(name)
|
|
|
|
exception = configexc.NoOptionError(
|
|
|
|
name, deleted=deleted, renamed=renamed)
|
|
|
|
raise exception from None
|
2014-04-10 18:01:16 +02:00
|
|
|
|
2018-02-16 11:18:43 +01:00
|
|
|
def get(self, name, url=None):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Get the given setting converted for Python code."""
|
2017-06-16 15:05:36 +02:00
|
|
|
opt = self.get_opt(name)
|
2018-02-18 20:06:43 +01:00
|
|
|
obj = self.get_obj(name, url=url)
|
2017-06-20 14:55:33 +02:00
|
|
|
return opt.typ.to_py(obj)
|
|
|
|
|
2018-02-18 19:11:12 +01:00
|
|
|
def _maybe_copy(self, value):
|
|
|
|
"""Copy the value if it could potentially be mutated."""
|
|
|
|
if isinstance(value, self.MUTABLE_TYPES):
|
2018-02-19 04:30:24 +01:00
|
|
|
# For mutable objects, create a copy so we don't accidentally
|
|
|
|
# mutate the config's internal value.
|
2018-02-18 19:11:12 +01:00
|
|
|
return copy.deepcopy(value)
|
|
|
|
else:
|
|
|
|
# Shouldn't be mutable (and thus hashable)
|
|
|
|
assert value.__hash__ is not None, value
|
|
|
|
return value
|
|
|
|
|
|
|
|
def get_obj(self, name, *, url=None):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Get the given setting as object (for YAML/config.py).
|
|
|
|
|
2018-02-18 19:11:12 +01:00
|
|
|
Note that the returned values are not watched for mutation.
|
2018-02-16 11:18:43 +01:00
|
|
|
If a URL is given, return the value which should be used for that URL.
|
2017-06-20 17:13:46 +02:00
|
|
|
"""
|
2018-02-19 21:22:26 +01:00
|
|
|
self.get_opt(name) # To make sure it exists
|
2018-02-18 19:11:12 +01:00
|
|
|
value = self._values[name].get_for_url(url)
|
|
|
|
return self._maybe_copy(value)
|
|
|
|
|
|
|
|
def get_obj_for_pattern(self, name, *, pattern):
|
|
|
|
"""Get the given setting as object (for YAML/config.py).
|
|
|
|
|
2018-02-19 04:30:24 +01:00
|
|
|
This gets the overridden value for a given pattern, or
|
|
|
|
configutils.UNSET if no such override exists.
|
2018-02-18 19:11:12 +01:00
|
|
|
"""
|
2018-02-19 21:22:26 +01:00
|
|
|
self.get_opt(name) # To make sure it exists
|
2018-02-18 19:11:12 +01:00
|
|
|
value = self._values[name].get_for_pattern(pattern, fallback=False)
|
|
|
|
return self._maybe_copy(value)
|
|
|
|
|
|
|
|
def get_mutable_obj(self, name, *, pattern=None):
|
|
|
|
"""Get an object which can be mutated, e.g. in a config.py.
|
|
|
|
|
|
|
|
If a pattern is given, return the value for that pattern.
|
|
|
|
Note that it's impossible to get a mutable object for an URL as we
|
|
|
|
wouldn't know what pattern to apply.
|
|
|
|
"""
|
2018-02-19 21:22:26 +01:00
|
|
|
self.get_opt(name) # To make sure it exists
|
|
|
|
|
2017-09-19 23:26:02 +02:00
|
|
|
# If we allow mutation, there is a chance that prior mutations already
|
|
|
|
# entered the mutable dictionary and thus further copies are unneeded
|
|
|
|
# until update_mutables() is called
|
2018-02-18 19:11:12 +01:00
|
|
|
if name in self._mutables:
|
2017-09-20 08:52:11 +02:00
|
|
|
_copy, obj = self._mutables[name]
|
2018-02-18 19:11:12 +01:00
|
|
|
return obj
|
|
|
|
|
|
|
|
value = self._values[name].get_for_pattern(pattern)
|
2018-02-19 07:14:48 +01:00
|
|
|
copy_value = self._maybe_copy(value)
|
2016-08-03 11:35:08 +02:00
|
|
|
|
2018-02-18 19:11:12 +01:00
|
|
|
# Watch the returned object for changes if it's mutable.
|
2018-02-19 07:14:48 +01:00
|
|
|
if isinstance(copy_value, self.MUTABLE_TYPES):
|
|
|
|
self._mutables[name] = (value, copy_value) # old, new
|
2018-02-18 19:11:12 +01:00
|
|
|
|
2018-02-19 07:14:48 +01:00
|
|
|
return copy_value
|
2016-08-03 11:35:08 +02:00
|
|
|
|
2018-02-16 11:18:43 +01:00
|
|
|
def get_str(self, name, *, pattern=None):
|
|
|
|
"""Get the given setting as string.
|
2018-02-15 22:54:22 +01:00
|
|
|
|
2018-02-16 11:18:43 +01:00
|
|
|
If a pattern is given, get the setting for the given pattern or
|
|
|
|
configutils.UNSET.
|
2018-02-15 22:54:22 +01:00
|
|
|
"""
|
2017-06-16 15:05:36 +02:00
|
|
|
opt = self.get_opt(name)
|
2018-02-16 11:18:43 +01:00
|
|
|
values = self._values[name]
|
|
|
|
value = values.get_for_pattern(pattern)
|
2017-06-19 13:00:10 +02:00
|
|
|
return opt.typ.to_str(value)
|
2016-08-03 11:35:08 +02:00
|
|
|
|
2018-02-15 21:28:05 +01:00
|
|
|
def set_obj(self, name, value, *, pattern=None, save_yaml=False):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Set the given setting from a YAML/config.py object.
|
|
|
|
|
|
|
|
If save_yaml=True is given, store the new value to YAML.
|
|
|
|
"""
|
2018-03-07 18:30:44 +01:00
|
|
|
opt = self.get_opt(name)
|
|
|
|
self._check_yaml(opt, save_yaml)
|
|
|
|
self._set_value(opt, value, pattern=pattern)
|
2017-06-20 16:53:46 +02:00
|
|
|
if save_yaml:
|
2018-02-15 21:28:05 +01:00
|
|
|
self._yaml.set_obj(name, value, pattern=pattern)
|
2017-06-19 16:41:17 +02:00
|
|
|
|
2018-02-15 21:28:05 +01:00
|
|
|
def set_str(self, name, value, *, pattern=None, save_yaml=False):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Set the given setting from a string.
|
|
|
|
|
|
|
|
If save_yaml=True is given, store the new value to YAML.
|
|
|
|
"""
|
2017-06-19 13:00:34 +02:00
|
|
|
opt = self.get_opt(name)
|
2018-03-07 18:30:44 +01:00
|
|
|
self._check_yaml(opt, save_yaml)
|
2017-06-20 16:53:46 +02:00
|
|
|
converted = opt.typ.from_str(value)
|
2017-09-14 14:15:06 +02:00
|
|
|
log.config.debug("Setting {} (type {}) to {!r} (converted from {!r})"
|
|
|
|
.format(name, opt.typ.__class__.__name__, converted,
|
|
|
|
value))
|
2018-02-15 21:28:05 +01:00
|
|
|
self._set_value(opt, converted, pattern=pattern)
|
2017-06-20 16:53:46 +02:00
|
|
|
if save_yaml:
|
2018-02-15 21:28:05 +01:00
|
|
|
self._yaml.set_obj(name, converted, pattern=pattern)
|
2014-04-09 22:44:07 +02:00
|
|
|
|
2018-02-15 21:28:05 +01:00
|
|
|
def unset(self, name, *, save_yaml=False, pattern=None):
|
2017-10-03 12:44:22 +02:00
|
|
|
"""Set the given setting back to its default."""
|
2018-03-07 18:30:44 +01:00
|
|
|
opt = self.get_opt(name)
|
|
|
|
self._check_yaml(opt, save_yaml)
|
2018-02-19 21:13:31 +01:00
|
|
|
changed = self._values[name].remove(pattern)
|
|
|
|
if changed:
|
|
|
|
self.changed.emit(name)
|
2017-10-03 12:44:22 +02:00
|
|
|
|
|
|
|
if save_yaml:
|
2018-02-15 21:28:05 +01:00
|
|
|
self._yaml.unset(name, pattern=pattern)
|
2017-10-03 12:44:22 +02:00
|
|
|
|
|
|
|
def clear(self, *, save_yaml=False):
|
|
|
|
"""Clear all settings in the config.
|
|
|
|
|
|
|
|
If save_yaml=True is given, also remove all customization from the YAML
|
|
|
|
file.
|
|
|
|
"""
|
2018-02-19 05:08:17 +01:00
|
|
|
for name, values in self._values.items():
|
2018-02-19 06:25:28 +01:00
|
|
|
if values:
|
|
|
|
values.clear()
|
2018-02-19 05:19:25 +01:00
|
|
|
self.changed.emit(name)
|
2017-10-03 12:44:22 +02:00
|
|
|
|
|
|
|
if save_yaml:
|
|
|
|
self._yaml.clear()
|
|
|
|
|
2017-06-20 16:53:46 +02:00
|
|
|
def update_mutables(self, *, save_yaml=False):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Update mutable settings if they changed.
|
|
|
|
|
|
|
|
Every time someone calls get_obj() on a mutable object, we save a
|
|
|
|
reference to the original object and a copy.
|
|
|
|
|
|
|
|
Here, we check all those saved copies for mutations, and if something
|
|
|
|
mutated, we call set_obj again so we save the new value.
|
|
|
|
"""
|
2017-09-20 08:52:11 +02:00
|
|
|
for name, (old_value, new_value) in self._mutables.items():
|
2017-06-20 14:55:33 +02:00
|
|
|
if old_value != new_value:
|
|
|
|
log.config.debug("{} was mutated, updating".format(name))
|
2017-06-20 16:53:46 +02:00
|
|
|
self.set_obj(name, new_value, save_yaml=save_yaml)
|
2017-09-17 23:07:52 +02:00
|
|
|
self._mutables = {}
|
2017-06-20 14:55:33 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
def dump_userconfig(self):
|
|
|
|
"""Get the part of the config which was changed by the user.
|
2014-02-26 07:44:39 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
Return:
|
|
|
|
The changed config part as string.
|
|
|
|
"""
|
2018-02-19 06:35:14 +01:00
|
|
|
blocks = []
|
2018-02-20 17:22:11 +01:00
|
|
|
for values in sorted(self, key=lambda v: v.opt.name):
|
2018-02-19 06:35:14 +01:00
|
|
|
if values:
|
|
|
|
blocks.append(str(values))
|
|
|
|
|
|
|
|
if not blocks:
|
2018-02-19 16:36:18 +01:00
|
|
|
return '<Default configuration>'
|
|
|
|
|
2018-02-19 06:35:14 +01:00
|
|
|
return '\n'.join(blocks)
|
2014-04-07 16:51:14 +02:00
|
|
|
|
2014-02-26 07:44:39 +01:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
class ConfigContainer:
|
2014-04-07 17:20:14 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
"""An object implementing config access via __getattr__.
|
2014-04-10 12:37:49 +02:00
|
|
|
|
|
|
|
Attributes:
|
2017-06-20 17:13:46 +02:00
|
|
|
_config: The Config object.
|
2017-06-16 15:05:36 +02:00
|
|
|
_prefix: The __getattr__ chain leading up to this object.
|
2017-09-14 22:47:06 +02:00
|
|
|
_configapi: If given, get values suitable for config.py and
|
|
|
|
add errors to the given ConfigAPI object.
|
2018-02-20 20:54:26 +01:00
|
|
|
_pattern: The URL pattern to be used.
|
2014-04-10 12:37:49 +02:00
|
|
|
"""
|
2014-03-27 22:37:34 +01:00
|
|
|
|
2018-02-20 20:54:26 +01:00
|
|
|
def __init__(self, config, configapi=None, prefix='', pattern=None):
|
2017-06-20 17:13:46 +02:00
|
|
|
self._config = config
|
2017-06-16 15:05:36 +02:00
|
|
|
self._prefix = prefix
|
2017-09-14 22:47:06 +02:00
|
|
|
self._configapi = configapi
|
2018-02-20 20:54:26 +01:00
|
|
|
self._pattern = pattern
|
|
|
|
if configapi is None and pattern is not None:
|
|
|
|
raise TypeError("Can't use pattern without configapi!")
|
2014-04-10 12:37:49 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
def __repr__(self):
|
2017-06-20 17:13:46 +02:00
|
|
|
return utils.get_repr(self, constructor=True, config=self._config,
|
2018-02-20 20:54:26 +01:00
|
|
|
configapi=self._configapi, prefix=self._prefix,
|
|
|
|
pattern=self._pattern)
|
2017-06-16 15:05:36 +02:00
|
|
|
|
2017-09-14 21:51:29 +02:00
|
|
|
@contextlib.contextmanager
|
|
|
|
def _handle_error(self, action, name):
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
except configexc.Error as e:
|
2017-09-14 22:47:06 +02:00
|
|
|
if self._configapi is None:
|
|
|
|
raise
|
|
|
|
text = "While {} '{}'".format(action, name)
|
|
|
|
self._configapi.errors.append(configexc.ConfigErrorDesc(text, e))
|
2017-09-14 21:51:29 +02:00
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
def __getattr__(self, attr):
|
|
|
|
"""Get an option or a new ConfigContainer with the added prefix.
|
|
|
|
|
|
|
|
If we get an option which exists, we return the value for it.
|
|
|
|
If we get a part of an option name, we return a new ConfigContainer.
|
|
|
|
|
|
|
|
Those two never overlap as configdata.py ensures there are no shadowing
|
|
|
|
options.
|
2014-04-10 12:37:49 +02:00
|
|
|
"""
|
2017-06-19 16:41:17 +02:00
|
|
|
if attr.startswith('_'):
|
|
|
|
return self.__getattribute__(attr)
|
|
|
|
|
2017-06-16 15:05:36 +02:00
|
|
|
name = self._join(attr)
|
|
|
|
if configdata.is_valid_prefix(name):
|
2017-09-14 22:47:06 +02:00
|
|
|
return ConfigContainer(config=self._config,
|
|
|
|
configapi=self._configapi,
|
2018-02-20 20:54:26 +01:00
|
|
|
prefix=name, pattern=self._pattern)
|
2017-06-19 16:41:17 +02:00
|
|
|
|
2017-09-14 21:51:29 +02:00
|
|
|
with self._handle_error('getting', name):
|
2017-09-14 22:47:06 +02:00
|
|
|
if self._configapi is None:
|
|
|
|
# access from Python code
|
2017-09-14 21:51:29 +02:00
|
|
|
return self._config.get(name)
|
2017-09-14 22:47:06 +02:00
|
|
|
else:
|
|
|
|
# access from config.py
|
2018-02-20 20:54:26 +01:00
|
|
|
return self._config.get_mutable_obj(
|
|
|
|
name, pattern=self._pattern)
|
2017-06-16 15:05:36 +02:00
|
|
|
|
|
|
|
def __setattr__(self, attr, value):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Set the given option in the config."""
|
2017-06-16 15:05:36 +02:00
|
|
|
if attr.startswith('_'):
|
2017-12-15 16:05:20 +01:00
|
|
|
super().__setattr__(attr, value)
|
|
|
|
return
|
2017-09-14 21:51:29 +02:00
|
|
|
|
|
|
|
name = self._join(attr)
|
|
|
|
with self._handle_error('setting', name):
|
2018-02-20 20:54:26 +01:00
|
|
|
self._config.set_obj(name, value, pattern=self._pattern)
|
2017-06-16 15:05:36 +02:00
|
|
|
|
|
|
|
def _join(self, attr):
|
2017-06-20 17:13:46 +02:00
|
|
|
"""Get the prefix joined with the given attribute."""
|
2017-06-16 15:05:36 +02:00
|
|
|
if self._prefix:
|
|
|
|
return '{}.{}'.format(self._prefix, attr)
|
|
|
|
else:
|
|
|
|
return attr
|
2014-04-10 12:37:49 +02:00
|
|
|
|
|
|
|
|
2017-07-02 16:05:04 +02:00
|
|
|
def set_register_stylesheet(obj, *, stylesheet=None, update=True):
|
|
|
|
"""Set the stylesheet for an object based on it's STYLESHEET attribute.
|
|
|
|
|
|
|
|
Also, register an update when the config is changed.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
obj: The object to set the stylesheet for and register.
|
|
|
|
Must have a STYLESHEET attribute if stylesheet is not given.
|
|
|
|
stylesheet: The stylesheet to use.
|
|
|
|
update: Whether to update the stylesheet on config changes.
|
|
|
|
"""
|
2017-11-13 18:04:31 +01:00
|
|
|
observer = StyleSheetObserver(obj, stylesheet, update)
|
|
|
|
observer.register()
|
2017-07-02 16:05:04 +02:00
|
|
|
|
|
|
|
|
2017-07-11 21:06:53 +02:00
|
|
|
@functools.lru_cache()
|
|
|
|
def _render_stylesheet(stylesheet):
|
|
|
|
"""Render the given stylesheet jinja template."""
|
2017-09-16 08:30:19 +02:00
|
|
|
with jinja.environment.no_autoescape():
|
|
|
|
template = jinja.environment.from_string(stylesheet)
|
2017-07-11 21:06:53 +02:00
|
|
|
return template.render(conf=val)
|
|
|
|
|
|
|
|
|
2017-07-02 16:05:04 +02:00
|
|
|
class StyleSheetObserver(QObject):
|
|
|
|
|
|
|
|
"""Set the stylesheet on the given object and update it on changes.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
_obj: The object to observe.
|
|
|
|
_stylesheet: The stylesheet template to use.
|
|
|
|
"""
|
|
|
|
|
2017-11-11 04:39:10 +01:00
|
|
|
def __init__(self, obj, stylesheet, update):
|
|
|
|
super().__init__()
|
2017-07-02 16:05:04 +02:00
|
|
|
self._obj = obj
|
2017-11-13 18:04:31 +01:00
|
|
|
self._update = update
|
2017-11-11 04:39:10 +01:00
|
|
|
|
|
|
|
# We only need to hang around if we are asked to update.
|
2017-11-13 18:04:31 +01:00
|
|
|
if self._update:
|
2017-11-11 04:39:10 +01:00
|
|
|
self.setParent(self._obj)
|
2017-07-02 16:05:04 +02:00
|
|
|
if stylesheet is None:
|
|
|
|
self._stylesheet = obj.STYLESHEET
|
|
|
|
else:
|
|
|
|
self._stylesheet = stylesheet
|
|
|
|
|
|
|
|
def _get_stylesheet(self):
|
|
|
|
"""Format a stylesheet based on a template.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
The formatted template as string.
|
|
|
|
"""
|
2017-07-11 21:06:53 +02:00
|
|
|
return _render_stylesheet(self._stylesheet)
|
2017-07-02 16:05:04 +02:00
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
def _update_stylesheet(self):
|
|
|
|
"""Update the stylesheet for obj."""
|
2017-07-02 18:01:37 +02:00
|
|
|
self._obj.setStyleSheet(self._get_stylesheet())
|
2017-07-02 16:05:04 +02:00
|
|
|
|
2017-11-13 18:04:31 +01:00
|
|
|
def register(self):
|
2017-07-02 16:05:04 +02:00
|
|
|
"""Do a first update and listen for more.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
update: if False, don't listen for future updates.
|
|
|
|
"""
|
|
|
|
qss = self._get_stylesheet()
|
|
|
|
log.config.vdebug("stylesheet for {}: {}".format(
|
|
|
|
self._obj.__class__.__name__, qss))
|
|
|
|
self._obj.setStyleSheet(qss)
|
2017-11-13 18:04:31 +01:00
|
|
|
if self._update:
|
2017-07-02 16:05:04 +02:00
|
|
|
instance.changed.connect(self._update_stylesheet)
|