Split config commands off to their own file.

This commit is contained in:
Florian Bruhin 2017-10-02 07:06:05 +02:00
parent 32d529b54e
commit a8fc561707
7 changed files with 570 additions and 495 deletions

View File

@ -23,13 +23,11 @@ import copy
import contextlib
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.config import configdata, configexc, configtypes
from qutebrowser.utils import utils, objreg, message, log, jinja
from qutebrowser.config import configdata, configexc
from qutebrowser.utils import utils, log, jinja
from qutebrowser.misc import objects
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.completion.models import configmodel
# An easy way to access the config from other code via config.val.foo
val = None
@ -205,148 +203,6 @@ class KeyConfig:
self._config.update_mutables(save_yaml=save_yaml)
class ConfigCommands:
"""qutebrowser commands related to the configuration."""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('values', completion=configmodel.value)
@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 until qutebrowser is closed.
print_: Print the value after setting.
"""
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 isinstance(opt.typ, configtypes.Bool):
values = ['false', 'true']
else:
raise cmdexc.CommandError(
"set: Can't toggle non-bool setting {}".format(option))
elif not values:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
self._set_next(option, values, temp=temp)
if print_:
self._print_value(option)
def _print_value(self, option):
"""Print the value of the given option."""
with self._handle_config_error():
value = self._config.get_str(option)
message.info("{} = {}".format(option, value))
def _set_next(self, option, values, *, temp):
"""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_str(option, values[0], save_yaml=not temp)
return
# Use the next valid value from values, or the first if the current
# value does not appear in the list
old_value = self._config.get_obj(option, mutable=False)
opt = self._config.get_opt(option)
values = [opt.typ.from_str(val) for val in values]
try:
idx = values.index(old_value)
idx = (idx + 1) % len(values)
value = values[idx]
except ValueError:
value = values[0]
self._config.set_obj(option, value, save_yaml=not temp)
@contextlib.contextmanager
def _handle_config_error(self):
"""Catch errors in set_command and raise CommandError."""
try:
yield
except configexc.Error as e:
raise cmdexc.CommandError("set: {}".format(e))
@cmdutils.register(instance='config-commands', maxsplit=1,
no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('command', completion=configmodel.bind)
def bind(self, key, command=None, *, mode='normal', force=False):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
command: The command to execute, with optional args, or None to
print the current binding.
mode: A comma-separated list of modes to bind the key in
(default: `normal`). See `:help bindings.commands` for the
available modes.
force: Rebind the key if it is already bound.
"""
if command is None:
if utils.is_special_key(key):
# self._keyconfig.get_command does this, but we also need it
# normalized for the output below
key = utils.normalize_keystr(key)
cmd = self._keyconfig.get_command(key, mode)
if cmd is None:
message.info("{} is unbound in {} mode".format(key, mode))
else:
message.info("{} is bound to '{}' in {} mode".format(
key, cmd, mode))
return
try:
self._keyconfig.bind(key, command, mode=mode, force=force,
save_yaml=True)
except configexc.DuplicateKeyError as e:
raise cmdexc.CommandError("bind: {} - use --force to override!"
.format(e))
except configexc.KeybindingError as e:
raise cmdexc.CommandError("bind: {}".format(e))
@cmdutils.register(instance='config-commands')
def unbind(self, key, *, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A mode to unbind the key in (default: `normal`).
See `:help bindings.commands` for the available modes.
"""
try:
self._keyconfig.unbind(key, mode=mode, save_yaml=True)
except configexc.KeybindingError as e:
raise cmdexc.CommandError('unbind: {}'.format(e))
class Config(QObject):
"""Main config object.

View File

@ -0,0 +1,171 @@
# 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/>.
"""Commands related to the configuration."""
import contextlib
from PyQt5.QtCore import QUrl
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.completion.models import configmodel
from qutebrowser.utils import objreg, utils, message
from qutebrowser.config import configtypes, configexc
class ConfigCommands:
"""qutebrowser commands related to the configuration."""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('values', completion=configmodel.value)
@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 until qutebrowser is closed.
print_: Print the value after setting.
"""
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 isinstance(opt.typ, configtypes.Bool):
values = ['false', 'true']
else:
raise cmdexc.CommandError(
"set: Can't toggle non-bool setting {}".format(option))
elif not values:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
self._set_next(option, values, temp=temp)
if print_:
self._print_value(option)
def _print_value(self, option):
"""Print the value of the given option."""
with self._handle_config_error():
value = self._config.get_str(option)
message.info("{} = {}".format(option, value))
def _set_next(self, option, values, *, temp):
"""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_str(option, values[0], save_yaml=not temp)
return
# Use the next valid value from values, or the first if the current
# value does not appear in the list
old_value = self._config.get_obj(option, mutable=False)
opt = self._config.get_opt(option)
values = [opt.typ.from_str(val) for val in values]
try:
idx = values.index(old_value)
idx = (idx + 1) % len(values)
value = values[idx]
except ValueError:
value = values[0]
self._config.set_obj(option, value, save_yaml=not temp)
@contextlib.contextmanager
def _handle_config_error(self):
"""Catch errors in set_command and raise CommandError."""
try:
yield
except configexc.Error as e:
raise cmdexc.CommandError("set: {}".format(e))
@cmdutils.register(instance='config-commands', maxsplit=1,
no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('command', completion=configmodel.bind)
def bind(self, key, command=None, *, mode='normal', force=False):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
command: The command to execute, with optional args, or None to
print the current binding.
mode: A comma-separated list of modes to bind the key in
(default: `normal`). See `:help bindings.commands` for the
available modes.
force: Rebind the key if it is already bound.
"""
if command is None:
if utils.is_special_key(key):
# self._keyconfig.get_command does this, but we also need it
# normalized for the output below
key = utils.normalize_keystr(key)
cmd = self._keyconfig.get_command(key, mode)
if cmd is None:
message.info("{} is unbound in {} mode".format(key, mode))
else:
message.info("{} is bound to '{}' in {} mode".format(
key, cmd, mode))
return
try:
self._keyconfig.bind(key, command, mode=mode, force=force,
save_yaml=True)
except configexc.DuplicateKeyError as e:
raise cmdexc.CommandError("bind: {} - use --force to override!"
.format(e))
except configexc.KeybindingError as e:
raise cmdexc.CommandError("bind: {}".format(e))
@cmdutils.register(instance='config-commands')
def unbind(self, key, *, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A mode to unbind the key in (default: `normal`).
See `:help bindings.commands` for the available modes.
"""
try:
self._keyconfig.unbind(key, mode=mode, save_yaml=True)
except configexc.KeybindingError as e:
raise cmdexc.CommandError('unbind: {}'.format(e))

View File

@ -25,7 +25,7 @@ import sys
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc)
configexc, configcommands)
from qutebrowser.utils import objreg, usertypes, log, standarddir, message
from qutebrowser.misc import msgbox, objects
@ -50,8 +50,8 @@ def early_init(args):
configtypes.Font.monospace_fonts = config.val.fonts.monospace
config_commands = config.ConfigCommands(config.instance,
config.key_instance)
config_commands = configcommands.ConfigCommands(
config.instance, config.key_instance)
objreg.register('config-commands', config_commands)
config_file = os.path.join(standarddir.config(), 'config.py')

View File

@ -143,6 +143,8 @@ PERFECT_FILES = [
'config/configtypes.py'),
('tests/unit/config/test_configinit.py',
'config/configinit.py'),
('tests/unit/config/test_configcommands.py',
'config/configcommands.py'),
('tests/unit/utils/test_qtutils.py',
'utils/qtutils.py'),

View File

@ -0,0 +1,29 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 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/>.
"""Fixtures needed in various config test files."""
import pytest
from qutebrowser.config import config
@pytest.fixture
def keyconf(config_stub):
config_stub.val.aliases = {}
return config.KeyConfig(config_stub)

View File

@ -22,12 +22,11 @@ import copy
import types
import pytest
from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QColor
from qutebrowser.commands import cmdexc
from qutebrowser.config import config, configdata, configexc
from qutebrowser.utils import objreg, usertypes
from qutebrowser.utils import usertypes
from qutebrowser.misc import objects
@ -38,12 +37,6 @@ def configdata_init():
configdata.init()
@pytest.fixture
def keyconf(config_stub):
config_stub.val.aliases = {}
return config.KeyConfig(config_stub)
class TestChangeFilter:
@pytest.fixture(autouse=True)
@ -262,342 +255,6 @@ class TestKeyConfig:
keyconf.unbind('foobar', mode='normal')
class TestSetConfigCommand:
"""Tests for :set."""
@pytest.fixture
def commands(self, config_stub, keyconf):
return config.ConfigCommands(config_stub, keyconf)
@pytest.fixture
def tabbed_browser(self, stubs, win_registry):
tb = stubs.TabbedBrowserStub()
objreg.register('tabbed-browser', tb, scope='window', window=0)
yield tb
objreg.delete('tabbed-browser', scope='window', window=0)
def test_set_no_args(self, commands, tabbed_browser):
"""Run ':set'.
Should open qute://settings."""
commands.set(win_id=0)
assert tabbed_browser.opened_url == QUrl('qute://settings')
def test_get(self, config_stub, commands, message_mock):
"""Run ':set url.auto_search?'.
Should show the value.
"""
config_stub.val.url.auto_search = 'never'
commands.set(win_id=0, option='url.auto_search?')
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = never'
@pytest.mark.parametrize('temp', [True, False])
@pytest.mark.parametrize('option, old_value, inp, new_value', [
('url.auto_search', 'naive', 'dns', 'dns'),
# https://github.com/qutebrowser/qutebrowser/issues/2962
('editor.command', ['gvim', '-f', '{}'], '[emacs, "{}"]',
['emacs', '{}']),
])
def test_set_simple(self, monkeypatch, commands, config_stub,
temp, option, old_value, inp, new_value):
"""Run ':set [-t] option value'.
Should set the setting accordingly.
"""
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
assert config_stub.get(option) == old_value
commands.set(0, option, inp, temp=temp)
assert config_stub.get(option) == new_value
if temp:
assert option not in config_stub._yaml
else:
assert config_stub._yaml[option] == new_value
@pytest.mark.parametrize('temp', [True, False])
def test_set_temp_override(self, commands, config_stub, temp):
"""Invoking :set twice.
:set url.auto_search dns
:set -t url.auto_search never
Should set the setting accordingly.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns')
commands.set(0, 'url.auto_search', 'never', temp=True)
assert config_stub.val.url.auto_search == 'never'
assert config_stub._yaml['url.auto_search'] == 'dns'
def test_set_print(self, config_stub, commands, message_mock):
"""Run ':set -p url.auto_search never'.
Should set show the value.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns', print_=True)
assert config_stub.val.url.auto_search == 'dns'
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = dns'
def test_set_toggle(self, commands, config_stub):
"""Run ':set auto_save.session!'.
Should toggle the value.
"""
assert not config_stub.val.auto_save.session
commands.set(0, 'auto_save.session!')
assert config_stub.val.auto_save.session
assert config_stub._yaml['auto_save.session']
def test_set_toggle_nonbool(self, commands, config_stub):
"""Run ':set url.auto_search!'.
Should show an error
"""
assert config_stub.val.url.auto_search == 'naive'
with pytest.raises(cmdexc.CommandError, match="set: Can't toggle "
"non-bool setting url.auto_search"):
commands.set(0, 'url.auto_search!')
assert config_stub.val.url.auto_search == 'naive'
def test_set_toggle_print(self, commands, config_stub, message_mock):
"""Run ':set -p auto_save.session!'.
Should toggle the value and show the new value.
"""
commands.set(0, 'auto_save.session!', print_=True)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'auto_save.session = true'
def test_set_invalid_option(self, commands):
"""Run ':set foo bar'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(0, 'foo', 'bar')
def test_set_invalid_value(self, commands):
"""Run ':set auto_save.session blah'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match="set: Invalid value 'blah' - must be a "
"boolean!"):
commands.set(0, 'auto_save.session', 'blah')
def test_set_wrong_backend(self, commands, monkeypatch):
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
with pytest.raises(cmdexc.CommandError,
match="set: This setting is not available with the "
"QtWebEngine backend!"):
commands.set(0, 'content.cookies.accept', 'all')
@pytest.mark.parametrize('option', ['?', '!', 'url.auto_search'])
def test_empty(self, commands, option):
"""Run ':set ?' / ':set !' / ':set url.auto_search'.
Should show an error.
See https://github.com/qutebrowser/qutebrowser/issues/1109
"""
with pytest.raises(cmdexc.CommandError,
match="set: The following arguments are required: "
"value"):
commands.set(win_id=0, option=option)
@pytest.mark.parametrize('suffix', '?!')
def test_invalid(self, commands, suffix):
"""Run ':set foo?' / ':set foo!'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(win_id=0, option='foo' + suffix)
@pytest.mark.parametrize('initial, expected', [
# Normal cycling
('magenta', 'blue'),
# Through the end of the list
('yellow', 'green'),
# Value which is not in the list
('red', 'green'),
])
def test_cycling(self, commands, config_stub, initial, expected):
"""Run ':set' with multiple values."""
opt = 'colors.statusbar.normal.bg'
config_stub.set_obj(opt, initial)
commands.set(0, opt, 'green', 'magenta', 'blue', 'yellow')
assert config_stub.get(opt) == expected
assert config_stub._yaml[opt] == expected
def test_cycling_different_representation(self, commands, config_stub):
"""When using a different representation, cycling should work.
For example, we use [foo] which is represented as ["foo"].
"""
opt = 'qt_args'
config_stub.set_obj(opt, ['foo'])
commands.set(0, opt, '[foo]', '[bar]')
assert config_stub.get(opt) == ['bar']
commands.set(0, opt, '[foo]', '[bar]')
assert config_stub.get(opt) == ['foo']
class TestBindConfigCommand:
"""Tests for :bind and :unbind."""
@pytest.fixture
def commands(self, config_stub, keyconf):
return config.ConfigCommands(config_stub, keyconf)
@pytest.fixture
def no_bindings(self):
"""Get a dict with no bindings."""
return {'normal': {}}
@pytest.mark.parametrize('command', ['nop', 'nope'])
def test_bind(self, commands, config_stub, no_bindings, keyconf, command):
"""Simple :bind test (and aliases)."""
config_stub.val.aliases = {'nope': 'nop'}
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
commands.bind('a', command)
assert keyconf.get_command('a', 'normal') == command
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
assert yaml_bindings['a'] == command
@pytest.mark.parametrize('key, mode, expected', [
# Simple
('a', 'normal', "a is bound to 'message-info a' in normal mode"),
# Alias
('b', 'normal', "b is bound to 'mib' in normal mode"),
# Custom binding
('c', 'normal', "c is bound to 'message-info c' in normal mode"),
# Special key
('<Ctrl-X>', 'normal',
"<ctrl+x> is bound to 'message-info C-x' in normal mode"),
# unbound
('x', 'normal', "x is unbound in normal mode"),
# non-default mode
('x', 'caret', "x is bound to 'nop' in caret mode"),
])
def test_bind_print(self, commands, config_stub, message_mock,
key, mode, expected):
"""Run ':bind key'.
Should print the binding.
"""
config_stub.val.aliases = {'mib': 'message-info b'}
config_stub.val.bindings.default = {
'normal': {'a': 'message-info a',
'b': 'mib',
'<Ctrl+x>': 'message-info C-x'},
'caret': {'x': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'c': 'message-info c'}
}
commands.bind(key, mode=mode)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == expected
def test_bind_invalid_mode(self, commands):
"""Run ':bind --mode=wrongmode nop'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match='bind: Invalid mode wrongmode!'):
commands.bind('a', 'nop', mode='wrongmode')
@pytest.mark.parametrize('force', [True, False])
@pytest.mark.parametrize('key', ['a', 'b', '<Ctrl-X>'])
def test_bind_duplicate(self, commands, config_stub, keyconf, force, key):
"""Run ':bind' with a key which already has been bound.'.
Also tests for https://github.com/qutebrowser/qutebrowser/issues/1544
"""
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<Ctrl+x>': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
}
if force:
commands.bind(key, 'message-info foo', mode='normal', force=True)
assert keyconf.get_command(key, 'normal') == 'message-info foo'
else:
with pytest.raises(cmdexc.CommandError,
match="bind: Duplicate key .* - use --force to "
"override"):
commands.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'nop'
def test_bind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
commands.bind(',x', 'nop')
def test_unbind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
commands.unbind('H')
@pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings
('b', 'b'), # custom bindings
('c', 'c'), # :bind then :unbind
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
])
def test_unbind(self, commands, keyconf, config_stub, key, normalized):
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
'caret': {'b': 'nop'},
}
if key == 'c':
# Test :bind and :unbind
commands.bind(key, 'nop')
commands.unbind(key)
assert keyconf.get_command(key, 'normal') is None
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
if key in 'bc':
# Custom binding
assert normalized not in yaml_bindings
else:
assert yaml_bindings[normalized] is None
@pytest.mark.parametrize('key, mode, expected', [
('foobar', 'normal',
"unbind: Can't find binding 'foobar' in normal mode"),
('x', 'wrongmode', "unbind: Invalid mode wrongmode!"),
])
def test_unbind_invalid(self, commands, key, mode, expected):
"""Run ':unbind foobar' / ':unbind x wrongmode'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match=expected):
commands.unbind(key, mode=mode)
class TestConfig:
@pytest.fixture

View File

@ -0,0 +1,360 @@
# 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/>.
"""Tests for qutebrowser.config.configcommands."""
import pytest
from PyQt5.QtCore import QUrl
from qutebrowser.config import configcommands
from qutebrowser.commands import cmdexc
from qutebrowser.utils import objreg, usertypes
from qutebrowser.misc import objects
@pytest.fixture
def commands(config_stub, keyconf):
return configcommands.ConfigCommands(config_stub, keyconf)
class TestSetConfigCommand:
"""Tests for :set."""
@pytest.fixture
def tabbed_browser(self, stubs, win_registry):
tb = stubs.TabbedBrowserStub()
objreg.register('tabbed-browser', tb, scope='window', window=0)
yield tb
objreg.delete('tabbed-browser', scope='window', window=0)
def test_set_no_args(self, commands, tabbed_browser):
"""Run ':set'.
Should open qute://settings."""
commands.set(win_id=0)
assert tabbed_browser.opened_url == QUrl('qute://settings')
def test_get(self, config_stub, commands, message_mock):
"""Run ':set url.auto_search?'.
Should show the value.
"""
config_stub.val.url.auto_search = 'never'
commands.set(win_id=0, option='url.auto_search?')
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = never'
@pytest.mark.parametrize('temp', [True, False])
@pytest.mark.parametrize('option, old_value, inp, new_value', [
('url.auto_search', 'naive', 'dns', 'dns'),
# https://github.com/qutebrowser/qutebrowser/issues/2962
('editor.command', ['gvim', '-f', '{}'], '[emacs, "{}"]',
['emacs', '{}']),
])
def test_set_simple(self, monkeypatch, commands, config_stub,
temp, option, old_value, inp, new_value):
"""Run ':set [-t] option value'.
Should set the setting accordingly.
"""
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
assert config_stub.get(option) == old_value
commands.set(0, option, inp, temp=temp)
assert config_stub.get(option) == new_value
if temp:
assert option not in config_stub._yaml
else:
assert config_stub._yaml[option] == new_value
@pytest.mark.parametrize('temp', [True, False])
def test_set_temp_override(self, commands, config_stub, temp):
"""Invoking :set twice.
:set url.auto_search dns
:set -t url.auto_search never
Should set the setting accordingly.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns')
commands.set(0, 'url.auto_search', 'never', temp=True)
assert config_stub.val.url.auto_search == 'never'
assert config_stub._yaml['url.auto_search'] == 'dns'
def test_set_print(self, config_stub, commands, message_mock):
"""Run ':set -p url.auto_search never'.
Should set show the value.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns', print_=True)
assert config_stub.val.url.auto_search == 'dns'
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = dns'
def test_set_toggle(self, commands, config_stub):
"""Run ':set auto_save.session!'.
Should toggle the value.
"""
assert not config_stub.val.auto_save.session
commands.set(0, 'auto_save.session!')
assert config_stub.val.auto_save.session
assert config_stub._yaml['auto_save.session']
def test_set_toggle_nonbool(self, commands, config_stub):
"""Run ':set url.auto_search!'.
Should show an error
"""
assert config_stub.val.url.auto_search == 'naive'
with pytest.raises(cmdexc.CommandError, match="set: Can't toggle "
"non-bool setting url.auto_search"):
commands.set(0, 'url.auto_search!')
assert config_stub.val.url.auto_search == 'naive'
def test_set_toggle_print(self, commands, config_stub, message_mock):
"""Run ':set -p auto_save.session!'.
Should toggle the value and show the new value.
"""
commands.set(0, 'auto_save.session!', print_=True)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'auto_save.session = true'
def test_set_invalid_option(self, commands):
"""Run ':set foo bar'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(0, 'foo', 'bar')
def test_set_invalid_value(self, commands):
"""Run ':set auto_save.session blah'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match="set: Invalid value 'blah' - must be a "
"boolean!"):
commands.set(0, 'auto_save.session', 'blah')
def test_set_wrong_backend(self, commands, monkeypatch):
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
with pytest.raises(cmdexc.CommandError,
match="set: This setting is not available with the "
"QtWebEngine backend!"):
commands.set(0, 'content.cookies.accept', 'all')
@pytest.mark.parametrize('option', ['?', '!', 'url.auto_search'])
def test_empty(self, commands, option):
"""Run ':set ?' / ':set !' / ':set url.auto_search'.
Should show an error.
See https://github.com/qutebrowser/qutebrowser/issues/1109
"""
with pytest.raises(cmdexc.CommandError,
match="set: The following arguments are required: "
"value"):
commands.set(win_id=0, option=option)
@pytest.mark.parametrize('suffix', '?!')
def test_invalid(self, commands, suffix):
"""Run ':set foo?' / ':set foo!'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(win_id=0, option='foo' + suffix)
@pytest.mark.parametrize('initial, expected', [
# Normal cycling
('magenta', 'blue'),
# Through the end of the list
('yellow', 'green'),
# Value which is not in the list
('red', 'green'),
])
def test_cycling(self, commands, config_stub, initial, expected):
"""Run ':set' with multiple values."""
opt = 'colors.statusbar.normal.bg'
config_stub.set_obj(opt, initial)
commands.set(0, opt, 'green', 'magenta', 'blue', 'yellow')
assert config_stub.get(opt) == expected
assert config_stub._yaml[opt] == expected
def test_cycling_different_representation(self, commands, config_stub):
"""When using a different representation, cycling should work.
For example, we use [foo] which is represented as ["foo"].
"""
opt = 'qt_args'
config_stub.set_obj(opt, ['foo'])
commands.set(0, opt, '[foo]', '[bar]')
assert config_stub.get(opt) == ['bar']
commands.set(0, opt, '[foo]', '[bar]')
assert config_stub.get(opt) == ['foo']
class TestBindConfigCommand:
"""Tests for :bind and :unbind."""
@pytest.fixture
def no_bindings(self):
"""Get a dict with no bindings."""
return {'normal': {}}
@pytest.mark.parametrize('command', ['nop', 'nope'])
def test_bind(self, commands, config_stub, no_bindings, keyconf, command):
"""Simple :bind test (and aliases)."""
config_stub.val.aliases = {'nope': 'nop'}
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
commands.bind('a', command)
assert keyconf.get_command('a', 'normal') == command
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
assert yaml_bindings['a'] == command
@pytest.mark.parametrize('key, mode, expected', [
# Simple
('a', 'normal', "a is bound to 'message-info a' in normal mode"),
# Alias
('b', 'normal', "b is bound to 'mib' in normal mode"),
# Custom binding
('c', 'normal', "c is bound to 'message-info c' in normal mode"),
# Special key
('<Ctrl-X>', 'normal',
"<ctrl+x> is bound to 'message-info C-x' in normal mode"),
# unbound
('x', 'normal', "x is unbound in normal mode"),
# non-default mode
('x', 'caret', "x is bound to 'nop' in caret mode"),
])
def test_bind_print(self, commands, config_stub, message_mock,
key, mode, expected):
"""Run ':bind key'.
Should print the binding.
"""
config_stub.val.aliases = {'mib': 'message-info b'}
config_stub.val.bindings.default = {
'normal': {'a': 'message-info a',
'b': 'mib',
'<Ctrl+x>': 'message-info C-x'},
'caret': {'x': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'c': 'message-info c'}
}
commands.bind(key, mode=mode)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == expected
def test_bind_invalid_mode(self, commands):
"""Run ':bind --mode=wrongmode nop'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match='bind: Invalid mode wrongmode!'):
commands.bind('a', 'nop', mode='wrongmode')
@pytest.mark.parametrize('force', [True, False])
@pytest.mark.parametrize('key', ['a', 'b', '<Ctrl-X>'])
def test_bind_duplicate(self, commands, config_stub, keyconf, force, key):
"""Run ':bind' with a key which already has been bound.'.
Also tests for https://github.com/qutebrowser/qutebrowser/issues/1544
"""
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<Ctrl+x>': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
}
if force:
commands.bind(key, 'message-info foo', mode='normal', force=True)
assert keyconf.get_command(key, 'normal') == 'message-info foo'
else:
with pytest.raises(cmdexc.CommandError,
match="bind: Duplicate key .* - use --force to "
"override"):
commands.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'nop'
def test_bind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
commands.bind(',x', 'nop')
def test_unbind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
commands.unbind('H')
@pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings
('b', 'b'), # custom bindings
('c', 'c'), # :bind then :unbind
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
])
def test_unbind(self, commands, keyconf, config_stub, key, normalized):
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
'caret': {'b': 'nop'},
}
if key == 'c':
# Test :bind and :unbind
commands.bind(key, 'nop')
commands.unbind(key)
assert keyconf.get_command(key, 'normal') is None
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
if key in 'bc':
# Custom binding
assert normalized not in yaml_bindings
else:
assert yaml_bindings[normalized] is None
@pytest.mark.parametrize('key, mode, expected', [
('foobar', 'normal',
"unbind: Can't find binding 'foobar' in normal mode"),
('x', 'wrongmode', "unbind: Invalid mode wrongmode!"),
])
def test_unbind_invalid(self, commands, key, mode, expected):
"""Run ':unbind foobar' / ':unbind x wrongmode'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match=expected):
commands.unbind(key, mode=mode)