Remove most legacy config code

This commit is contained in:
Florian Bruhin 2017-06-16 15:05:36 +02:00
parent 785de9fb99
commit bc8176ff21
8 changed files with 334 additions and 1640 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,335 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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 = ['<Default configuration>']
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()

View File

@ -1,74 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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)

View File

@ -1,231 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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()

View File

@ -1,39 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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)

View File

@ -1,101 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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

View File

@ -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."""

View File

@ -1,38 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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