diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 555a7a22b..26dff2b3c 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -17,135 +17,366 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Configuration storage and config-related utilities. +"""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 inherits from configparser, -but we borrow some methods and classes from there where it makes sense. -""" - -import re -import os -import sys import os.path +import contextlib import functools import configparser -import contextlib -import collections -import collections.abc from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings -from PyQt5.QtGui import QColor -from qutebrowser.config import configdata, configexc, textwrapper, newconfig -from qutebrowser.config.parsers import keyconf -from qutebrowser.config.parsers import ini +from qutebrowser.config import configdata, configexc, configtypes +from qutebrowser.utils import utils, objreg, message, standarddir from qutebrowser.commands import cmdexc, cmdutils -from qutebrowser.utils import (message, objreg, utils, standarddir, log, - qtutils, error, usertypes) -from qutebrowser.misc import objects -from qutebrowser.utils.usertypes import Completion - -# FIXME:conf compat -from qutebrowser.config.newconfig import change_filter -UNSET = object() - - -# FIXME:conf for new config +# An easy way to access the config from other code via config.val.foo val = None instance = None key_instance = None +# Keeping track of all change filters to validate them later. +_change_filters = [] -def _init_main_config(parent=None): - """Initialize the main config. + +class change_filter: # pylint: disable=invalid-name + + """Decorator to filter calls based on a config section/option matching. + + This could also be a function, but as a class (with a "wrong" name) it's + much cleaner to implement. + + Attributes: + _option: An option or prefix to be filtered + _function: Whether a function rather than a method is decorated. + """ + + def __init__(self, option, function=False): + """Save decorator arguments. + + Gets called on parse-time with the decorator arguments. + + Args: + option: The option to be filtered. + function: Whether a function rather than a method is decorated. + """ + self._option = option + self._function = function + _change_filters.append(self) + + def validate(self): + """Make sure the configured option or prefix exists. + + 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 + + def __call__(self, func): + """Filter calls to the decorated function. + + Gets called when a function should be decorated. + + Adds a filter which returns if we're not interested in the change-event + and calls the wrapped function if we are. + + We assume the function passed doesn't take any parameters. + + Args: + func: The function to be decorated. + + Return: + The decorated function. + """ + if self._function: + @functools.wraps(func) + def wrapper(option=None): + if self._check_match(option): + return func() + else: + @functools.wraps(func) + def wrapper(wrapper_self, option=None): + if self._check_match(option): + return func(wrapper_self) + + return wrapper + + +class NewKeyConfig: + + def get_reverse_bindings_for(self, section): + """Get a dict of commands to a list of bindings for the section.""" + cmd_to_keys = {} + bindings = val.bindings.commands[section] + if bindings is None: + return cmd_to_keys + for key, full_cmd in bindings.items(): + for cmd in full_cmd.split(';;'): + cmd = cmd.strip() + cmd_to_keys.setdefault(cmd, []) + # put special bindings last + if utils.is_special_key(key): + cmd_to_keys[cmd].append(key) + else: + cmd_to_keys[cmd].insert(0, key) + return cmd_to_keys + + +class ConfigCommands: + + def __init__(self, config): + self._config = config + + @cmdutils.register(instance='config-commands', star_args_optional=True) + @cmdutils.argument('win_id', win_id=True) + def set(self, win_id, option=None, *values, temp=False, print_=False): + """Set an option. + + If the option name ends with '?', the value of the option is shown + instead. + + If the option name ends with '!' and it is a boolean value, toggle it. + + // + + Args: + option: The name of the option. + values: The value to set, or the values to cycle through. + temp: Set value temporarily. + print_: Print the value after setting. + """ + # FIXME:conf write to YAML if temp isn't used! + if option is None: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + tabbed_browser.openurl(QUrl('qute://settings'), newtab=False) + return + + if option.endswith('?') and option != '?': + self._print_value(option[:-1]) + return + + with self._handle_config_error(): + if option.endswith('!') and option != '!' and not values: + # Handle inversion as special cases of the cycle code path + option = option[:-1] + opt = self._config.get_opt(option) + if opt.typ is configtypes.Bool: + values = ['false', 'true'] + else: + raise cmdexc.CommandError( + "set: Attempted inversion of non-boolean value.") + elif not values: + raise cmdexc.CommandError("set: The following arguments " + "are required: value") + self._set_next(option, values) + + if print_: + self._print_value(option) + + def _print_value(self, option): + """Print the value of the given option.""" + with self._handle_config_error(): + val = self._config.get_str(option) + message.info("{} = {}".format(option, val)) + + def _set_next(self, option, values): + """Set the next value out of a list of values.""" + if len(values) == 1: + # If we have only one value, just set it directly (avoid + # breaking stuff like aliases or other pseudo-settings) + self._config.set(option, values[0]) + return + + # Use the next valid value from values, or the first if the current + # value does not appear in the list + val = self._config.get_str(option) + try: + idx = values.index(str(val)) + idx = (idx + 1) % len(values) + value = values[idx] + except ValueError: + value = values[0] + self._config.set(option, value) + + @contextlib.contextmanager + def _handle_config_error(self): + """Catch errors in set_command and raise CommandError.""" + try: + yield + except (configexc.NoOptionError, configexc.ValidationError) as e: + raise cmdexc.CommandError("set: {}".format(e)) + except configexc.Error as e: + raise cmdexc.CommandError("set: {} - {}".format( + e.__class__.__name__, e)) + + +class NewConfigManager(QObject): + + changed = pyqtSignal(str) # FIXME:conf stub... + + def __init__(self, parent=None): + super().__init__(parent) + self.options = {} + self._values = {} # FIXME:conf stub + + def read_defaults(self): + for name, option in configdata.DATA.items(): + self.options[name] = option + + def get_opt(self, name): + try: + return self.options[name] + except KeyError: + raise configexc.NoOptionError(name) + + def get(self, name): + opt = self.get_opt(name) + value = self._values.get(name, opt.default) + return opt.typ.to_py(value) + + def get_str(self, name): + opt = self.get_opt(name) + return opt.typ.to_str(opt.default) + + def set(self, name, value): + # FIXME:conf stub + try: + opt = self.options[name] + except KeyError: + raise configexc.NoOptionError(name) + self._values[name] = opt.typ.from_str(value) + self.changed.emit(name) + + def dump_userconfig(self): + """Get the part of the config which was changed by the user. + + Return: + The changed config part as string. + """ + lines = ['{} = {}'.format(optname, value) + for optname, value in self._values.items()] + if not lines: + lines = [''] + return '\n'.join(lines) + + +class ConfigContainer: + + """An object implementing config access via __getattr__. + + Attributes: + _manager: The ConfigManager object. + _prefix: The __getattr__ chain leading up to this object. + """ + + def __init__(self, manager, prefix=''): + self._manager = manager + self._prefix = prefix + + def __repr__(self): + return utils.get_repr(self, constructor=True, manager=self._manager, + prefix=self._prefix) + + 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. + """ + name = self._join(attr) + if configdata.is_valid_prefix(name): + return ConfigContainer(manager=self._manager, prefix=name) + try: + return self._manager.get(name) + except configexc.NoOptionError as e: + # If it's not a valid prefix - re-raise to improve error text. + raise configexc.NoOptionError(name) + + def __setattr__(self, attr, value): + if attr.startswith('_'): + return super().__setattr__(attr, value) + self._handler(self._join(attr), value) + + def _join(self, attr): + if self._prefix: + return '{}.{}'.format(self._prefix, attr) + else: + return attr + + +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) + + +def init(parent=None): + """Initialize the config. Args: - parent: The parent to pass to ConfigManager. + parent: The parent to pass to QObjects which get initialized. """ - args = objreg.get('args') - config_obj = ConfigManager(parent=parent) - try: - config_obj.read(standarddir.config(), 'qutebrowser.conf', - relaxed=args.relaxed_config) - except (configexc.Error, configparser.Error, UnicodeDecodeError) as e: - log.init.exception(e) - errstr = "Error while reading config:" - try: - errstr += "\n\n{} -> {}:".format( - e.section, e.option) - except AttributeError: - pass - errstr += "\n" - error.handle_fatal_exc(e, args, "Error while reading config!", - pre_text=errstr) - # We didn't really initialize much so far, so we just quit hard. - sys.exit(usertypes.Exit.err_config) - else: - objreg.register('config', config_obj) + configdata.init() - save_manager = objreg.get('save-manager') - save_manager.add_saveable( - 'config', config_obj.save, config_obj.changed, - config_opt='auto_save.config', filename=filename) - for sect in config_obj.sections.values(): - for opt in sect.values.values(): - if opt.values['conf'] is None: - # Option added to built-in defaults but not in user's - # config yet - save_manager.save('config', explicit=True, force=True) - return + new_config = NewConfigManager(parent) + new_config.read_defaults() + objreg.register('config', new_config) + config_commands = ConfigCommands(new_config) + objreg.register('config-commands', config_commands) -def _init_key_config(parent): - """Initialize the key config. + global val, instance, key_instance + val = ConfigContainer(new_config) + instance = new_config + key_instance = NewKeyConfig() - Args: - parent: The parent to use for the KeyConfigParser. - """ - args = objreg.get('args') - try: - key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf', - args.relaxed_config, - parent=parent) - except (keyconf.KeyConfigError, cmdexc.CommandError, - UnicodeDecodeError) as e: - log.init.exception(e) - errstr = "Error while reading key config:\n" - if e.lineno is not None: - errstr += "In line {}: ".format(e.lineno) - error.handle_fatal_exc(e, args, "Error while reading key config!", - pre_text=errstr) - # We didn't really initialize much so far, so we just quit hard. - sys.exit(usertypes.Exit.err_key_config) - else: - objreg.register('key-config', key_config) - save_manager = objreg.get('save-manager') - filename = os.path.join(standarddir.config(), 'keys.conf') - save_manager.add_saveable( - 'key-config', key_config.save, key_config.config_dirty, - config_opt='auto_save.config', filename=filename, - dirty=key_config.is_dirty) + for cf in _change_filters: + cf.validate() - -def _init_misc(): - """Initialize misc. config-related files.""" - save_manager = objreg.get('save-manager') - state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state') - for sect in ['general', 'geometry']: - try: - state_config.add_section(sect) - except configparser.DuplicateSectionError: - pass - # See commit a98060e020a4ba83b663813a4b9404edb47f28ad. - state_config['general'].pop('fooled', None) - objreg.register('state-config', state_config) - save_manager.add_saveable('state-config', state_config.save) + 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', @@ -164,623 +395,3 @@ def _init_misc(): path = os.path.join(standarddir.config(), 'qsettings') for fmt in [QSettings.NativeFormat, QSettings.IniFormat]: QSettings.setPath(fmt, QSettings.UserScope, path) - - -def init(parent=None): - """Initialize the config. - - Args: - parent: The parent to pass to QObjects which get initialized. - """ - global val, instance, key_instance - # _init_main_config(parent) - configdata.init() - newconfig.init(parent) - val = newconfig.val - instance = newconfig.instance - key_instance = newconfig.key_instance - # _init_key_config(parent) - _init_misc() - - -def _get_value_transformer(mapping): - """Get a function which transforms a value for CHANGED_OPTIONS. - - Args: - mapping: A dictionary mapping old values to new values. Value is not - transformed if the supplied value doesn't match the old value. - - Return: - A function which takes a value and transforms it. - """ - def transformer(val): - try: - return mapping[val] - except KeyError: - return val - return transformer - - -def _transform_position(val): - """Transformer for position values.""" - mapping = { - 'north': 'top', - 'south': 'bottom', - 'west': 'left', - 'east': 'right', - } - try: - return mapping[val] - except KeyError: - return val - - -def _transform_hint_color(val): - """Transformer for hint colors.""" - log.config.debug("Transforming hint value {}".format(val)) - - def to_rgba(qcolor): - """Convert a QColor to a rgba() value.""" - return 'rgba({}, {}, {}, 0.8)'.format(qcolor.red(), qcolor.green(), - qcolor.blue()) - - if val.startswith('-webkit-gradient'): - pattern = re.compile(r'-webkit-gradient\(linear, left top, ' - r'left bottom, ' - r'color-stop\(0%, *([^)]*)\), ' - r'color-stop\(100%, *([^)]*)\)\)') - - match = pattern.fullmatch(val) - if match: - log.config.debug('Color groups: {}'.format(match.groups())) - start_color = QColor(match.group(1)) - stop_color = QColor(match.group(2)) - if not start_color.isValid() or not stop_color.isValid(): - return None - - return ('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {}, ' - 'stop:1 {})'.format(to_rgba(start_color), - to_rgba(stop_color))) - else: - return None - elif val.startswith('-'): # Custom CSS stuff? - return None - else: # Already transformed or a named color. - return val - - -def _transform_hint_font(val): - """Transformer for fonts -> hints.""" - match = re.fullmatch(r'(.*\d+p[xt]) Monospace', val) - if match: - # Close enough to the old default: - return match.group(1) + ' ${_monospace}' - else: - return val - - -class ConfigManager(QObject): - - """Configuration manager for qutebrowser. - - Class attributes: - KEY_ESCAPE: Chars which need escaping when they occur as first char - in a line. - ESCAPE_CHAR: The char to be used for escaping - RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'} - RENAMED_OPTIONS: A mapping of renamed options, - {('section', 'oldname'): 'newname'} - CHANGED_OPTIONS: A mapping of arbitrarily changed options, - {('section', 'option'): callable}. - The callable takes the old value and returns the new - one. - DELETED_OPTIONS: A (section, option) list of deleted options. - - Attributes: - sections: The configuration data as an OrderedDict. - _fname: The filename to be opened. - _configdir: The dictionary to read the config from and save it in. - _interpolation: A configparser.Interpolation object - _proxies: configparser.SectionProxy objects for sections. - _initialized: Whether the ConfigManager is fully initialized yet. - - Signals: - changed: Emitted when a config option changed. - style_changed: When style caches need to be invalidated. - Args: the changed section and option. - """ - - KEY_ESCAPE = r'\#[' - ESCAPE_CHAR = '\\' - RENAMED_SECTIONS = { - 'permissions': 'content' - } - RENAMED_OPTIONS = { - ('colors', 'tab.fg.odd'): 'tabs.fg.odd', - ('colors', 'tab.fg.even'): 'tabs.fg.even', - ('colors', 'tab.fg.selected'): 'tabs.fg.selected.odd', - ('colors', 'tabs.fg.selected'): 'tabs.fg.selected.odd', - ('colors', 'tab.bg.odd'): 'tabs.bg.odd', - ('colors', 'tab.bg.even'): 'tabs.bg.even', - ('colors', 'tab.bg.selected'): 'tabs.bg.selected.odd', - ('colors', 'tabs.bg.selected'): 'tabs.bg.selected.odd', - ('colors', 'tab.bg.bar'): 'tabs.bg.bar', - ('colors', 'tab.indicator.start'): 'tabs.indicator.start', - ('colors', 'tab.indicator.stop'): 'tabs.indicator.stop', - ('colors', 'tab.indicator.error'): 'tabs.indicator.error', - ('colors', 'tab.indicator.system'): 'tabs.indicator.system', - ('completion', 'history-length'): 'cmd-history-max-items', - ('colors', 'downloads.fg'): 'downloads.fg.start', - ('ui', 'show-keyhints'): 'keyhint-blacklist', - ('content', 'javascript-can-open-windows'): - 'javascript-can-open-windows-automatically', - ('colors', 'statusbar.fg.error'): 'messages.fg.error', - ('colors', 'statusbar.bg.error'): 'messages.bg.error', - ('colors', 'statusbar.fg.warning'): 'messages.fg.warning', - ('colors', 'statusbar.bg.warning'): 'messages.bg.warning', - ('colors', 'statusbar.fg.prompt'): 'prompts.fg', - ('colors', 'statusbar.bg.prompt'): 'prompts.bg', - ('storage', 'offline-web-application-storage'): - 'offline-web-application-cache', - } - DELETED_OPTIONS = [ - ('colors', 'tab.separator'), - ('colors', 'tabs.separator'), - ('colors', 'tab.seperator'), # pragma: no spellcheck - ('colors', 'tabs.seperator'), # pragma: no spellcheck - ('colors', 'completion.item.bg'), - ('tabs', 'indicator-space'), - ('tabs', 'hide-auto'), - ('tabs', 'auto-hide'), - ('tabs', 'hide-always'), - ('ui', 'display-statusbar-messages'), - ('ui', 'hide-mouse-cursor'), - ('ui', 'css-media-type'), - ('general', 'wrap-search'), - ('general', 'site-specific-quirks'), - ('hints', 'opacity'), - ('completion', 'auto-open'), - ('storage', 'object-cache-capacities'), - ('storage', 'offline-storage-database'), - ('storage', 'offline-storage-default-quota'), - ('storage', 'offline-web-application-cache-quota'), - ('content', 'css-regions'), - ] - CHANGED_OPTIONS = { - ('content', 'cookies-accept'): - _get_value_transformer({'default': 'no-3rdparty'}), - ('tabs', 'new-tab-position'): - _get_value_transformer({ - 'left': 'prev', - 'right': 'next'}), - ('tabs', 'new-tab-position-explicit'): - _get_value_transformer({ - 'left': 'prev', - 'right': 'next'}), - ('tabs', 'position'): _transform_position, - ('tabs', 'select-on-remove'): - _get_value_transformer({ - 'left': 'prev', - 'right': 'next', - 'previous': 'last-used'}), - ('ui', 'downloads-position'): _transform_position, - ('ui', 'remove-finished-downloads'): - _get_value_transformer({'false': '-1', 'true': '1000'}), - ('general', 'log-javascript-console'): - _get_value_transformer({'false': 'none', 'true': 'debug'}), - ('ui', 'keyhint-blacklist'): - _get_value_transformer({'false': '*', 'true': ''}), - ('hints', 'auto-follow'): - _get_value_transformer({'false': 'never', 'true': 'unique-match'}), - ('colors', 'hints.bg'): _transform_hint_color, - ('colors', 'hints.fg'): _transform_hint_color, - ('colors', 'hints.fg.match'): _transform_hint_color, - ('fonts', 'hints'): _transform_hint_font, - ('completion', 'show'): - _get_value_transformer({'false': 'never', 'true': 'always'}), - ('ui', 'user-stylesheet'): - _get_value_transformer({ - 'html > ::-webkit-scrollbar { width: 0px; height: 0px; }': '', - '::-webkit-scrollbar { width: 0px; height: 0px; }': '', - }), - ('general', 'default-encoding'): - _get_value_transformer({'': 'iso-8859-1'}), - ('contents', 'cache-size'): - _get_value_transformer({'52428800': ''}), - ('storage', 'maximum-pages-in-cache'): - _get_value_transformer({'': '0'}), - ('fonts', 'web-size-minimum'): - _get_value_transformer({'': '0'}), - ('fonts', 'web-size-minimum-logical'): - _get_value_transformer({'': '6'}), - ('fonts', 'web-size-default'): - _get_value_transformer({'': '16'}), - ('fonts', 'web-size-default-fixed'): - _get_value_transformer({'': '13'}), - } - - changed = pyqtSignal(str, str) - style_changed = pyqtSignal(str, str) - - def __init__(self, parent=None): - super().__init__(parent) - self._initialized = False - self._configdir = None - self._fname = None - self.sections = configdata.data() - self._interpolation = configparser.ExtendedInterpolation() - self._proxies = {} - for sectname in self.sections: - self._proxies[sectname] = SectionProxy(self, sectname) - - def __getitem__(self, key): - """Get a section from the config.""" - return self._proxies[key] - - def __repr__(self): - return utils.get_repr(self, fname=self._fname) - - def __str__(self): - """Get the whole config as a string.""" - lines = configdata.FIRST_COMMENT.strip('\n').splitlines() - for sectname, sect in self.sections.items(): - lines += ['\n'] + self._str_section_desc(sectname) - lines.append('[{}]'.format(sectname)) - lines += self._str_items(sectname, sect) - return '\n'.join(lines) + '\n' - - def _str_section_desc(self, sectname): - """Get the section description string for sectname.""" - wrapper = textwrapper.TextWrapper() - lines = [] - seclines = configdata.SECTION_DESC[sectname].splitlines() - for secline in seclines: - if 'http://' in secline or 'https://' in secline: - lines.append('# ' + secline) - else: - lines += wrapper.wrap(secline) - return lines - - def _str_items(self, sectname, sect): - """Get the option items as string for sect.""" - lines = [] - for optname, option in sect.items(): - value = option.value(startlayer='conf') - for c in self.KEY_ESCAPE: - if optname.startswith(c): - optname = optname.replace(c, self.ESCAPE_CHAR + c, 1) - # configparser can't handle = in keys :( - optname = optname.replace('=', '') - keyval = '{} = {}'.format(optname, value) - lines += self._str_option_desc(sectname, sect, optname, option) - lines.append(keyval) - return lines - - def _str_option_desc(self, sectname, sect, optname, option): - """Get the option description strings for a single option.""" - wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5, - subsequent_indent='#' + ' ' * 5) - lines = [] - if not getattr(sect, 'descriptions', None): - return lines - - lines.append('') - typestr = ' ({})'.format(option.typ.get_name()) - lines.append("# {}{}:".format(optname, typestr)) - - try: - desc = self.sections[sectname].descriptions[optname] - except KeyError: - log.config.exception("No description for {}.{}!".format( - sectname, optname)) - return [] - for descline in desc.splitlines(): - lines += wrapper.wrap(descline) - valid_values = option.typ.get_valid_values() - if valid_values is not None: - if valid_values.descriptions: - for val in valid_values: - desc = valid_values.descriptions[val] - lines += wrapper.wrap(" {}: {}".format(val, desc)) - else: - lines += wrapper.wrap("Valid values: {}".format(', '.join( - valid_values))) - lines += wrapper.wrap("Default: {}".format( - option.values['default'])) - return lines - - def _get_real_sectname(self, cp, sectname): - """Get an old or new section name based on a configparser. - - This checks if sectname is in cp, and if not, migrates it if needed and - tries again. - - Args: - cp: The configparser to check. - sectname: The new section name. - - Returns: - The section name in the configparser as a string, or None if the - configparser doesn't contain the section. - """ - reverse_renamed_sections = {v: k for k, v in - self.RENAMED_SECTIONS.items()} - if sectname in reverse_renamed_sections: - old_sectname = reverse_renamed_sections[sectname] - else: - old_sectname = sectname - if old_sectname in cp: - return old_sectname - elif sectname in cp: - return sectname - else: - return None - - def _from_cp(self, cp, relaxed=False): - """Read the config from a configparser instance. - - Args: - cp: The configparser instance to read the values from. - relaxed: Whether to ignore inexistent sections/options. - """ - for sectname in cp: - if sectname in self.RENAMED_SECTIONS: - sectname = self.RENAMED_SECTIONS[sectname] - if sectname != 'DEFAULT' and sectname not in self.sections: - if not relaxed: - raise configexc.NoSectionError(sectname) - for sectname in self.sections: - self._from_cp_section(sectname, cp, relaxed) - - def _from_cp_section(self, sectname, cp, relaxed): - """Read a single section from a configparser instance. - - Args: - sectname: The name of the section to read. - cp: The configparser instance to read the values from. - relaxed: Whether to ignore inexistent options. - """ - real_sectname = self._get_real_sectname(cp, sectname) - if real_sectname is None: - return - for k, v in cp[real_sectname].items(): - if k.startswith(self.ESCAPE_CHAR): - k = k[1:] - - if (sectname, k) in self.DELETED_OPTIONS: - continue - if (sectname, k) in self.RENAMED_OPTIONS: - k = self.RENAMED_OPTIONS[sectname, k] - if (sectname, k) in self.CHANGED_OPTIONS: - func = self.CHANGED_OPTIONS[(sectname, k)] - new_v = func(v) - if new_v is None: - exc = configexc.ValidationError( - v, "Could not automatically migrate the given value") - exc.section = sectname - exc.option = k - raise exc - - v = new_v - - try: - self.set('conf', sectname, k, v, validate=False) - except configexc.NoOptionError: - if relaxed: - pass - else: - raise - - def _validate_all(self): - """Validate all values set in self._from_cp.""" - for sectname, sect in self.sections.items(): - mapping = {key: val.value() for key, val in sect.values.items()} - for optname, opt in sect.items(): - interpolated = self._interpolation.before_get( - self, sectname, optname, opt.value(), mapping) - try: - opt.typ.validate(interpolated) - except configexc.ValidationError as e: - e.section = sectname - e.option = optname - raise - - def _changed(self, sectname, optname): - """Notify other objects the config has changed.""" - log.config.debug("Config option changed: {} -> {}".format( - sectname, optname)) - if sectname in ['colors', 'fonts']: - self.style_changed.emit(sectname, optname) - self.changed.emit(sectname, optname) - - def _after_set(self, changed_sect, changed_opt): - """Clean up caches and emit signals after an option has been set.""" - self.get.cache_clear() - self._changed(changed_sect, changed_opt) - # Options in the same section and ${optname} interpolation. - for optname, option in self.sections[changed_sect].items(): - if '${' + changed_opt + '}' in option.value(): - self._changed(changed_sect, optname) - # Options in any section and ${sectname:optname} interpolation. - for sectname, sect in self.sections.items(): - for optname, option in sect.items(): - if ('${' + changed_sect + ':' + changed_opt + '}' in - option.value()): - self._changed(sectname, optname) - - def read(self, configdir, fname, relaxed=False): - """Read the config from the given directory/file.""" - self._fname = fname - self._configdir = configdir - parser = ini.ReadConfigParser(configdir, fname) - self._from_cp(parser, relaxed) - self._initialized = True - self._validate_all() - - def items(self, sectname, raw=True): - """Get a list of (optname, value) tuples for a section. - - Implemented for configparser interpolation compatibility - - Args: - sectname: The name of the section to get. - raw: Whether to get raw values. Note this parameter only exists - for ConfigParser compatibility and raw=False is not supported. - """ - items = [] - if not raw: - raise ValueError("items() with raw=True is not implemented!") - for optname, option in self.sections[sectname].items(): - items.append((optname, option.value())) - return items - - def has_option(self, sectname, optname): - """Check if option exists in section. - - Args: - sectname: The section name. - optname: The option name - - Return: - True if the option and section exist, False otherwise. - """ - if sectname not in self.sections: - return False - return optname in self.sections[sectname] - - def remove_option(self, sectname, optname): - """Remove an option. - - Args: - sectname: The section where to remove an option. - optname: The option name to remove. - - Return: - True if the option existed, False otherwise. - """ - try: - sectdict = self.sections[sectname] - except KeyError: - raise configexc.NoSectionError(sectname) - optname = self.optionxform(optname) - existed = optname in sectdict - if existed: - sectdict.delete(optname) - self.get.cache_clear() - return existed - - def set(self, layer, sectname, optname, value, validate=True): - """Set an option. - - Args: - layer: A layer name as string (conf/temp/default). - sectname: The name of the section to change. - optname: The name of the option to change. - value: The new value. - validate: Whether to validate the value immediately. - """ - try: - value = self._interpolation.before_set(self, sectname, optname, - value) - except ValueError as e: - raise configexc.InterpolationSyntaxError(optname, sectname, str(e)) - try: - sect = self.sections[sectname] - except KeyError: - raise configexc.NoSectionError(sectname) - mapping = {key: val.value() for key, val in sect.values.items()} - - if validate: - interpolated = self._interpolation.before_get( - self, sectname, optname, value, mapping) - try: - allowed_backends = sect.values[optname].backends - except KeyError: - # Will be handled later in .setv() - pass - else: - if (allowed_backends is not None and - objects.backend not in allowed_backends): - raise configexc.BackendError(objects.backend) - else: - interpolated = None - - try: - sect.setv(layer, optname, value, interpolated) - except KeyError: - raise configexc.NoOptionError(optname, sectname) - else: - if self._initialized: - self._after_set(sectname, optname) - - def save(self): - """Save the config file.""" - configfile = os.path.join(self._configdir, self._fname) - log.destroy.debug("Saving config to {}".format(configfile)) - with qtutils.savefile_open(configfile) as f: - f.write(str(self)) - - def optionxform(self, val): - """Implemented to be compatible with ConfigParser interpolation.""" - return val - - -class SectionProxy(collections.abc.MutableMapping): - - """A proxy for a single section from a config. - - Attributes: - _conf: The Config object. - _name: The section name. - """ - - def __init__(self, conf, name): - """Create a view on a section. - - Args: - conf: The Config object. - name: The section name. - """ - self.conf = conf - self.name = name - - def __repr__(self): - return utils.get_repr(self, name=self.name) - - def __getitem__(self, key): - if not self.conf.has_option(self.name, key): - raise KeyError(key) - return self.conf.get(self.name, key) - - def __setitem__(self, key, value): - return self.conf.set('conf', self.name, key, value) - - def __delitem__(self, key): - if not (self.conf.has_option(self.name, key) and - self.conf.remove_option(self.name, key)): - raise KeyError(key) - - def __contains__(self, key): - return self.conf.has_option(self.name, key) - - def __len__(self): - return len(self._options()) - - def __iter__(self): - return self._options().__iter__() - - def _options(self): - """Get the option keys from this section.""" - return self.conf.sections[self.name].keys() - - def get(self, optname, *, raw=False): # pylint: disable=arguments-differ - """Get a value from this section. - - We deliberately don't support the default argument here, but have a raw - argument instead. - - Args: - optname: The option name to get. - raw: Whether to get a raw value or not. - """ - return self.conf.get(self.name, optname, raw=raw) diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py deleted file mode 100644 index 5d8b02cca..000000000 --- a/qutebrowser/config/newconfig.py +++ /dev/null @@ -1,335 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# 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 . - -"""New qutebrowser configuration code.""" - -import contextlib -import functools - -from PyQt5.QtCore import pyqtSignal, QObject, QUrl - -from qutebrowser.config import configdata, configexc, configtypes -from qutebrowser.utils import utils, objreg, message -from qutebrowser.commands import cmdexc, cmdutils - -# An easy way to access the config from other code via config.val.foo -val = None -instance = None - -_change_filters = [] - - -class change_filter: # pylint: disable=invalid-name - - """Decorator to filter calls based on a config section/option matching. - - This could also be a function, but as a class (with a "wrong" name) it's - much cleaner to implement. - - Attributes: - _option: An option or prefix to be filtered - _function: Whether a function rather than a method is decorated. - """ - - def __init__(self, option, function=False): - """Save decorator arguments. - - Gets called on parse-time with the decorator arguments. - - Args: - option: The option to be filtered. - function: Whether a function rather than a method is decorated. - """ - self._option = option - self._function = function - _change_filters.append(self) - - def validate(self): - """Make sure the configured option or prefix exists. - - 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 - - def __call__(self, func): - """Filter calls to the decorated function. - - Gets called when a function should be decorated. - - Adds a filter which returns if we're not interested in the change-event - and calls the wrapped function if we are. - - We assume the function passed doesn't take any parameters. - - Args: - func: The function to be decorated. - - Return: - The decorated function. - """ - if self._function: - @functools.wraps(func) - def wrapper(option=None): - if self._check_match(option): - return func() - else: - @functools.wraps(func) - def wrapper(wrapper_self, option=None): - if self._check_match(option): - return func(wrapper_self) - - return wrapper - - -class NewKeyConfig: - - def get_reverse_bindings_for(self, section): - """Get a dict of commands to a list of bindings for the section.""" - cmd_to_keys = {} - bindings = val.bindings.commands[section] - if bindings is None: - return cmd_to_keys - for key, full_cmd in bindings.items(): - for cmd in full_cmd.split(';;'): - cmd = cmd.strip() - cmd_to_keys.setdefault(cmd, []) - # put special bindings last - if utils.is_special_key(key): - cmd_to_keys[cmd].append(key) - else: - cmd_to_keys[cmd].insert(0, key) - return cmd_to_keys - - -class ConfigCommands: - - def __init__(self, config): - self._config = config - - @cmdutils.register(instance='config-commands', star_args_optional=True) - @cmdutils.argument('win_id', win_id=True) - def set(self, win_id, option=None, *values, temp=False, print_=False): - """Set an option. - - If the option name ends with '?', the value of the option is shown - instead. - - If the option name ends with '!' and it is a boolean value, toggle it. - - // - - Args: - option: The name of the option. - values: The value to set, or the values to cycle through. - temp: Set value temporarily. - print_: Print the value after setting. - """ - # FIXME:conf write to YAML if temp isn't used! - if option is None: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) - tabbed_browser.openurl(QUrl('qute://settings'), newtab=False) - return - - if option.endswith('?') and option != '?': - self._print_value(option[:-1]) - return - - with self._handle_config_error(): - if option.endswith('!') and option != '!' and not values: - # Handle inversion as special cases of the cycle code path - option = option[:-1] - opt = self._config.get_opt(option) - if opt.typ is configtypes.Bool: - values = ['false', 'true'] - else: - raise cmdexc.CommandError( - "set: Attempted inversion of non-boolean value.") - elif not values: - raise cmdexc.CommandError("set: The following arguments " - "are required: value") - self._set_next(option, values) - - if print_: - self._print_value(option) - - def _print_value(self, option): - """Print the value of the given option.""" - with self._handle_config_error(): - val = self._config.get_str(option) - message.info("{} = {}".format(option, val)) - - def _set_next(self, option, values): - """Set the next value out of a list of values.""" - if len(values) == 1: - # If we have only one value, just set it directly (avoid - # breaking stuff like aliases or other pseudo-settings) - self._config.set(option, values[0]) - return - - # Use the next valid value from values, or the first if the current - # value does not appear in the list - val = self._config.get_str(option) - try: - idx = values.index(str(val)) - idx = (idx + 1) % len(values) - value = values[idx] - except ValueError: - value = values[0] - self._config.set(option, value) - - @contextlib.contextmanager - def _handle_config_error(self): - """Catch errors in set_command and raise CommandError.""" - try: - yield - except (configexc.NoOptionError, configexc.ValidationError) as e: - raise cmdexc.CommandError("set: {}".format(e)) - except configexc.Error as e: - raise cmdexc.CommandError("set: {} - {}".format( - e.__class__.__name__, e)) - - -class NewConfigManager(QObject): - - changed = pyqtSignal(str) # FIXME:conf stub... - - def __init__(self, parent=None): - super().__init__(parent) - self.options = {} - self._values = {} # FIXME:conf stub - - def read_defaults(self): - for name, option in configdata.DATA.items(): - self.options[name] = option - - def get_opt(self, name): - try: - return self.options[name] - except KeyError: - raise configexc.NoOptionError(name) - - def get(self, name): - opt = self.get_opt(name) - value = self._values.get(name, opt.default) - return opt.typ.to_py(value) - - def get_str(self, name): - opt = self.get_opt(name) - return opt.typ.to_str(opt.default) - - def set(self, name, value): - # FIXME:conf stub - try: - opt = self.options[name] - except KeyError: - raise configexc.NoOptionError(name) - self._values[name] = opt.typ.from_str(value) - self.changed.emit(name) - - def dump_userconfig(self): - """Get the part of the config which was changed by the user. - - Return: - The changed config part as string. - """ - lines = ['{} = {}'.format(optname, value) - for optname, value in self._values.items()] - if not lines: - lines = [''] - return '\n'.join(lines) - - -class ConfigContainer: - - """An object implementing config access via __getattr__. - - Attributes: - _manager: The ConfigManager object. - _prefix: The __getattr__ chain leading up to this object. - """ - - def __init__(self, manager, prefix=''): - self._manager = manager - self._prefix = prefix - - def __repr__(self): - return utils.get_repr(self, constructor=True, manager=self._manager, - prefix=self._prefix) - - 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. - """ - name = self._join(attr) - if configdata.is_valid_prefix(name): - return ConfigContainer(manager=self._manager, prefix=name) - try: - return self._manager.get(name) - except configexc.NoOptionError as e: - # If it's not a valid prefix - re-raise to improve error text. - raise configexc.NoOptionError(name) - - def __setattr__(self, attr, value): - if attr.startswith('_'): - return super().__setattr__(attr, value) - self._handler(self._join(attr), value) - - def _join(self, attr): - if self._prefix: - return '{}.{}'.format(self._prefix, attr) - else: - return attr - - -def init(parent): - new_config = NewConfigManager(parent) - new_config.read_defaults() - objreg.register('config', new_config) - - config_commands = ConfigCommands(new_config) - objreg.register('config-commands', config_commands) - - global val, instance, key_instance - val = ConfigContainer(new_config) - instance = new_config - key_instance = NewKeyConfig() - - for cf in _change_filters: - cf.validate() diff --git a/qutebrowser/config/parsers/ini.py b/qutebrowser/config/parsers/ini.py deleted file mode 100644 index 0ae485f4b..000000000 --- a/qutebrowser/config/parsers/ini.py +++ /dev/null @@ -1,74 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# 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 . - -"""Parsers for INI-like config files, based on Python's ConfigParser.""" - -import os -import os.path -import configparser - -from qutebrowser.utils import log, utils, qtutils - - -class ReadConfigParser(configparser.ConfigParser): - - """Our own ConfigParser subclass to read the main config. - - Attributes: - _configdir: The directory to read the config from. - _fname: The filename of the config. - _configfile: The config file path. - """ - - def __init__(self, configdir, fname): - """Config constructor. - - Args: - configdir: Directory to read the config from. - fname: Filename of the config file. - """ - super().__init__(interpolation=None, comment_prefixes='#') - self.optionxform = lambda opt: opt # be case-insensitive - self._configdir = configdir - self._fname = fname - self._configfile = os.path.join(self._configdir, fname) - - if not os.path.isfile(self._configfile): - return - log.init.debug("Reading config from {}".format(self._configfile)) - self.read(self._configfile, encoding='utf-8') - - def __repr__(self): - return utils.get_repr(self, constructor=True, - configdir=self._configdir, fname=self._fname) - - -class ReadWriteConfigParser(ReadConfigParser): - - """ConfigParser subclass used for auxiliary config files.""" - - def save(self): - """Save the config file.""" - if self._configdir is None: - return - if not os.path.exists(self._configdir): - os.makedirs(self._configdir, 0o755) - log.destroy.debug("Saving config to {}".format(self._configfile)) - with qtutils.savefile_open(self._configfile) as f: - self.write(f) diff --git a/qutebrowser/config/sections.py b/qutebrowser/config/sections.py deleted file mode 100644 index 04a735647..000000000 --- a/qutebrowser/config/sections.py +++ /dev/null @@ -1,231 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# 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 . - -"""Setting sections used for qutebrowser.""" - -import collections - -from qutebrowser.config import value as confvalue - - -class Section: - - """Base class for KeyValue/ValueList sections. - - Attributes: - _readonly: Whether this section is read-only. - values: An OrderedDict with key as index and value as value. - key: string - value: SettingValue - descriptions: A dict with the description strings for the keys. - """ - - def __init__(self): - self.values = None - self.descriptions = {} - self._readonly = False - - def __getitem__(self, key): - """Get the value for key. - - Args: - key: The key to get a value for, as a string. - - Return: - The value, as value class. - """ - return self.values[key] - - def __iter__(self): - """Iterate over all set values.""" - return iter(self.values) - - def __bool__(self): - """Get boolean state of section.""" - return bool(self.values) - - def __contains__(self, key): - """Return whether the section contains a given key.""" - return key in self.values - - def items(self): - """Get dict items.""" - return self.values.items() - - def keys(self): - """Get value keys.""" - return self.values.keys() - - def delete(self, key): - """Delete item with given key.""" - del self.values[key] - - def setv(self, layer, key, value, interpolated): - """Set the value on a layer. - - Args: - layer: The layer to set the value on, an element name of the - ValueLayers dict. - key: The key of the element to set. - value: The value to set. - interpolated: The interpolated value, for checking, or None. - """ - raise NotImplementedError - - def dump_userconfig(self): - """Dump the part of the config which was changed by the user. - - Return: - A list of (key, valuestr) tuples. - """ - raise NotImplementedError - - -class KeyValue(Section): - - """Representation of a section with ordinary key-value mappings. - - This is a section which contains normal "key = value" pairs with a fixed - set of keys. - """ - - def __init__(self, *defaults, readonly=False): - """Constructor. - - Args: - *defaults: A (key, value, description) list of defaults. - readonly: Whether this config is readonly. - """ - super().__init__() - self._readonly = readonly - if not defaults: - return - self.values = collections.OrderedDict() - for (k, v, desc) in defaults: - assert k not in self.values, k - self.values[k] = v - self.descriptions[k] = desc - - def setv(self, layer, key, value, interpolated): - if self._readonly: - raise ValueError("Trying to modify a read-only config!") - self.values[key].setv(layer, value, interpolated) - - def dump_userconfig(self): - changed = [] - for k, v in self.items(): - vals = v.values - if vals['temp'] is not None and vals['temp'] != vals['default']: - changed.append((k, vals['temp'])) - elif vals['conf'] is not None and vals['conf'] != vals['default']: - changed.append((k, vals['conf'])) - return changed - - -class ValueList(Section): - - """This class represents a section with a list key-value settings. - - These are settings inside sections which don't have fixed keys, but instead - have a dynamic list of "key = value" pairs, like key bindings or - searchengines. - - They basically consist of two different SettingValues. - - Attributes: - layers: An OrderedDict of the config layers. - keytype: The type to use for the key (only used for validating) - valtype: The type to use for the value. - _ordered_value_cache: A ChainMap-like OrderedDict of all values. - _readonly: Whether this section is read-only. - """ - - def __init__(self, keytype, valtype, *defaults, readonly=False): - """Wrap types over default values. Take care when overriding this. - - Args: - keytype: The type instance to be used for keys. - valtype: The type instance to be used for values. - *defaults: A (key, value) list of default values. - readonly: Whether this config is readonly. - """ - super().__init__() - self._readonly = readonly - self._ordered_value_cache = None - self.keytype = keytype - self.valtype = valtype - self.layers = collections.OrderedDict([ - ('default', collections.OrderedDict()), - ('conf', collections.OrderedDict()), - ('temp', collections.OrderedDict()), - ]) - defaultlayer = self.layers['default'] - for key, value in defaults: - assert key not in defaultlayer, key - defaultlayer[key] = confvalue.SettingValue(valtype, value) - self.values = collections.ChainMap( - self.layers['temp'], self.layers['conf'], self.layers['default']) - - def _ordered_values(self): - """Get ordered values in layers. - - This is more expensive than the ChainMap, but we need this for - iterating/items/etc. when order matters. - """ - if self._ordered_value_cache is None: - self._ordered_value_cache = collections.OrderedDict() - for layer in self.layers.values(): - self._ordered_value_cache.update(layer) - return self._ordered_value_cache - - def setv(self, layer, key, value, interpolated): - if self._readonly: - raise ValueError("Trying to modify a read-only config!") - self.keytype.validate(key) - if key in self.layers[layer]: - self.layers[layer][key].setv(layer, value, interpolated) - else: - val = confvalue.SettingValue(self.valtype) - val.setv(layer, value, interpolated) - self.layers[layer][key] = val - self._ordered_value_cache = None - - def dump_userconfig(self): - changed = [] - mapping = collections.ChainMap(self.layers['temp'], - self.layers['conf']) - for k, v in mapping.items(): - try: - if v.value() != self.layers['default'][k].value(): - changed.append((k, v.value())) - except KeyError: - changed.append((k, v.value())) - return changed - - def __iter__(self): - """Iterate over all set values.""" - return self._ordered_values().__iter__() - - def items(self): - """Get dict items.""" - return self._ordered_values().items() - - def keys(self): - """Get value keys.""" - return self._ordered_values().keys() diff --git a/qutebrowser/config/textwrapper.py b/qutebrowser/config/textwrapper.py deleted file mode 100644 index b5744f60b..000000000 --- a/qutebrowser/config/textwrapper.py +++ /dev/null @@ -1,39 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# 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 . - -"""Textwrapper used for config files.""" - -import textwrap - - -class TextWrapper(textwrap.TextWrapper): - - """Text wrapper customized to be used in configs.""" - - def __init__(self, **kwargs): - kw = { - 'width': 72, - 'replace_whitespace': False, - 'break_long_words': False, - 'break_on_hyphens': False, - 'initial_indent': '# ', - 'subsequent_indent': '# ', - } - kw.update(kwargs) - super().__init__(**kw) diff --git a/qutebrowser/config/value.py b/qutebrowser/config/value.py deleted file mode 100644 index b23674606..000000000 --- a/qutebrowser/config/value.py +++ /dev/null @@ -1,101 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# 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 . - -"""A single value (with multiple layers possibly) in the config.""" - -import collections - - -class SettingValue: - - """Base class for setting values. - - Intended to be sub-classed by config value "types". - - Attributes: - typ: A BaseType subclass instance. - value: (readonly property) The currently valid, most important value. - values: An OrderedDict with the values on different layers, with the - most significant layer first. - """ - - def __init__(self, typ, default=None, *, backends=None): - """Constructor. - - Args: - typ: The BaseType to use. - default: Raw value to set. - backend: A list of usertypes.Backend enum members to mark this - setting as unsupported with other backends. - """ - self.typ = typ - self.values = collections.OrderedDict.fromkeys( - ['temp', 'conf', 'default']) - self.values['default'] = default - self.backends = backends - - def __str__(self): - """Get raw string value.""" - return self.value() - - def default(self): - """Get the default value.""" - return self.values['default'] - - def getlayers(self, startlayer): - """Get a dict of values starting with startlayer. - - Args: - startlayer: The first layer to include. - """ - idx = list(self.values.keys()).index(startlayer) - d = collections.OrderedDict(list(self.values.items())[idx:]) - return d - - def value(self, startlayer=None): - """Get the first valid value starting from startlayer. - - Args: - startlayer: The first layer to include. - """ - if startlayer is None: - d = self.values - else: - d = self.getlayers(startlayer) - for val in d.values(): - if val is not None: - return val - raise ValueError("No valid config value found!") - - def transformed(self): - """Get the transformed value.""" - return self.typ.transform(self.value()) - - def setv(self, layer, value, interpolated): - """Set the value on a layer. - - Args: - layer: The layer to set the value on, an element name of the - ValueLayers dict. - value: The value to set. - interpolated: The interpolated value, for typechecking (or None). - """ - if interpolated is not None: - self.typ.validate(interpolated) - self.values[layer] = value diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index c897b9f16..da5daea10 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -81,29 +81,6 @@ class TestConfigParser: with pytest.raises(configexc.ValidationError): objects.cfg._validate_all() - @pytest.mark.parametrize('config, sect1, opt1, sect2, opt2', [ - # Same section - ({'general': {'ignore-case': 'false', - 'private-browsing': '${ignore-case}'}}, - 'general', 'ignore-case', 'general', 'private-browsing'), - # Across sections - ({'general': {'ignore-case': '${network:do-not-track}'}, - 'network': {'do-not-track': 'false'}}, - 'general', 'ignore-case', 'network', 'do-not-track'), - ]) - def test_interpolation(self, objects, config, sect1, opt1, sect2, opt2): - objects.cp.read_dict(config) - objects.cfg._from_cp(objects.cp) - assert not objects.cfg.get(sect1, opt1) - assert not objects.cfg.get(sect2, opt2) - - def test_invalid_interpolation(self, objects): - """Test an invalid interpolation.""" - objects.cp.read_dict({'general': {'ignore-case': '${foo}'}}) - objects.cfg._from_cp(objects.cp) - with pytest.raises(configparser.InterpolationError): - objects.cfg._validate_all() - @pytest.mark.parametrize('config, exception', [ # Invalid interpolation syntax ({'general': {'ignore-case': '${'}}, @@ -131,26 +108,6 @@ class TestConfigParser: with pytest.raises(exception): objects.cfg.get(sect, opt) - def test_fallback(self, objects): - """Test getting an option with fallback. - - This is done during interpolation in later Python 3.4 versions. - - See https://github.com/qutebrowser/qutebrowser/issues/968 - """ - assert objects.cfg.get('general', 'blabla', fallback='blub') == 'blub' - - def test_sectionproxy(self, objects): - """Test getting an option via the section proxy.""" - objects.cp.read_dict({'general': {'ignore-case': 'false'}}) - objects.cfg._from_cp(objects.cp) - assert not objects.cfg['general'].get('ignore-case') - - def test_sectionproxy_keyerror(self, objects): - """Test getting an inexistent option via the section proxy.""" - with pytest.raises(configexc.NoOptionError): - objects.cfg['general'].get('blahblahblub') - @pytest.mark.parametrize('old_sect, new_sect', config.ConfigManager.RENAMED_SECTIONS.items()) def test_renamed_sections(self, old_sect, new_sect): @@ -184,62 +141,6 @@ class TestConfigParser: assert objects.cfg.get('general', 'save-session') -class TestTransformers: - - """Test value transformers in CHANGED_OPTIONS.""" - - @pytest.mark.parametrize('val, expected', [('a', 'b'), ('c', 'c')]) - def test_get_value_transformer(self, val, expected): - func = config._get_value_transformer({'a': 'b'}) - assert func(val) == expected - - @pytest.mark.parametrize('val, expected', [ - ('top', 'top'), - ('north', 'top'), - ('south', 'bottom'), - ('west', 'left'), - ('east', 'right'), - ]) - def test_position(self, val, expected): - func = config._transform_position - assert func(val) == expected - - OLD_GRADIENT = ('-webkit-gradient(linear, left top, left bottom, ' - 'color-stop(0%,{}), color-stop(100%,{}))') - NEW_GRADIENT = ('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {}, ' - 'stop:1 {})') - - @pytest.mark.parametrize('val, expected', [ - ('-unknown-stuff', None), - ('blue', 'blue'), - ('rgba(1, 2, 3, 4)', 'rgba(1, 2, 3, 4)'), - ('-webkit-gradient(unknown)', None), - (OLD_GRADIENT.format('blah', 'blah'), None), - (OLD_GRADIENT.format('red', 'green'), - NEW_GRADIENT.format('rgba(255, 0, 0, 0.8)', 'rgba(0, 128, 0, 0.8)')), - (OLD_GRADIENT.format(' red', ' green'), - NEW_GRADIENT.format('rgba(255, 0, 0, 0.8)', 'rgba(0, 128, 0, 0.8)')), - (OLD_GRADIENT.format('#101010', ' #202020'), - NEW_GRADIENT.format('rgba(16, 16, 16, 0.8)', - 'rgba(32, 32, 32, 0.8)')), - (OLD_GRADIENT.format('#666', ' #777'), - NEW_GRADIENT.format('rgba(102, 102, 102, 0.8)', - 'rgba(119, 119, 119, 0.8)')), - (OLD_GRADIENT.format('red', 'green') + 'more stuff', None), - ]) - def test_hint_color(self, val, expected): - assert config._transform_hint_color(val) == expected - - @pytest.mark.parametrize('val, expected', [ - ('bold 12pt Monospace', 'bold 12pt ${_monospace}'), - ('23pt Monospace', '23pt ${_monospace}'), - ('bold 12pt ${_monospace}', 'bold 12pt ${_monospace}'), - ('bold 12pt Comic Sans MS', 'bold 12pt Comic Sans MS'), - ]) - def test_hint_font(self, val, expected): - assert config._transform_hint_font(val) == expected - - class TestKeyConfigParser: """Test config.parsers.keyconf.KeyConfigParser.""" diff --git a/tests/unit/config/test_textwrapper.py b/tests/unit/config/test_textwrapper.py deleted file mode 100644 index a654f9367..000000000 --- a/tests/unit/config/test_textwrapper.py +++ /dev/null @@ -1,38 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2015-2017 Florian Bruhin (The Compiler) -# -# 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 . - -"""Tests for config.textwrapper.""" - -from qutebrowser.config import textwrapper - - -def test_default_args(): - wrapper = textwrapper.TextWrapper() - assert wrapper.width == 72 - assert not wrapper.replace_whitespace - assert not wrapper.break_long_words - assert not wrapper.break_on_hyphens - assert wrapper.initial_indent == '# ' - assert wrapper.subsequent_indent == '# ' - - -def test_custom_args(): - wrapper = textwrapper.TextWrapper(drop_whitespace=False) - assert wrapper.width == 72 - assert not wrapper.drop_whitespace