From a8fc5617075ebc4aff5dbdae7f804f36eb7fc9cb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Oct 2017 07:06:05 +0200 Subject: [PATCH] Split config commands off to their own file. --- qutebrowser/config/config.py | 150 +--------- qutebrowser/config/configcommands.py | 171 +++++++++++ qutebrowser/config/configinit.py | 6 +- scripts/dev/check_coverage.py | 2 + tests/unit/config/conftest.py | 29 ++ tests/unit/config/test_config.py | 347 +--------------------- tests/unit/config/test_configcommands.py | 360 +++++++++++++++++++++++ 7 files changed, 570 insertions(+), 495 deletions(-) create mode 100644 qutebrowser/config/configcommands.py create mode 100644 tests/unit/config/conftest.py create mode 100644 tests/unit/config/test_configcommands.py diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 891e56bf5..7107564af 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -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. diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py new file mode 100644 index 000000000..866f29c74 --- /dev/null +++ b/qutebrowser/config/configcommands.py @@ -0,0 +1,171 @@ +# 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 . + +"""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)) diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 2bbed56e5..209e6428d 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -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') diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 99bd1277d..708870371 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -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'), diff --git a/tests/unit/config/conftest.py b/tests/unit/config/conftest.py new file mode 100644 index 000000000..89767b786 --- /dev/null +++ b/tests/unit/config/conftest.py @@ -0,0 +1,29 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: +# Copyright 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 . + +"""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) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 43af839ba..24a0965d0 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -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 - ('', 'normal', - " 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', - '': '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', '']) - 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', '': '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 - ('', '') # normalized special binding - ]) - def test_unbind(self, commands, keyconf, config_stub, key, normalized): - config_stub.val.bindings.default = { - 'normal': {'a': 'nop', '': 'nop'}, - 'caret': {'a': 'nop', '': '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 diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py new file mode 100644 index 000000000..a3862e027 --- /dev/null +++ b/tests/unit/config/test_configcommands.py @@ -0,0 +1,360 @@ +# 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 . + +"""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 + ('', 'normal', + " 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', + '': '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', '']) + 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', '': '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 + ('', '') # normalized special binding + ]) + def test_unbind(self, commands, keyconf, config_stub, key, normalized): + config_stub.val.bindings.default = { + 'normal': {'a': 'nop', '': 'nop'}, + 'caret': {'a': 'nop', '': '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)