Merge branch 'config-write-py'

This commit is contained in:
Florian Bruhin 2017-10-05 11:30:50 +02:00
commit 618586f8b0
13 changed files with 421 additions and 354 deletions

View File

@ -67,6 +67,7 @@ Added
- `:config-clear` to remove all configured options
- `:config-source` to (re-)read a `config.py` file
- `:config-edit` to open the `config.py` file in an editor
- `:config-write-py` to write a `config.py` template file
- New `:version` command which opens `qute://version`.
Changed

View File

@ -36,6 +36,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<config-edit,config-edit>>|Open the config.py file in the editor.
|<<config-source,config-source>>|Read a config.py file.
|<<config-unset,config-unset>>|Unset an option.
|<<config-write-py,config-write-py>>|Write the current configuration to a config.py file.
|<<download,download>>|Download a given URL, or current page if no URL given.
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|<<download-clear,download-clear>>|Remove all finished downloads from the list.
@ -266,6 +267,19 @@ This sets an option back to its default and removes it from autoconfig.yml.
==== optional arguments
* +*-t*+, +*--temp*+: Don't touch autoconfig.yml.
[[config-write-py]]
=== config-write-py
Syntax: +:config-write-py [*--force*] [*--defaults*] ['filename']+
Write the current configuration to a config.py file.
==== positional arguments
* +'filename'+: The file to write to, or not given for the default config.py.
==== optional arguments
* +*-f*+, +*--force*+: Force overwriting existing files.
* +*-d*+, +*--defaults*+: Write the defaults instead of values configured via :set.
[[download]]
=== download
Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url'] ['dest-old']+

View File

@ -68,6 +68,12 @@ will never be used in a default keybinding.
See the help pages linked above (or `:help :bind`, `:help :unbind`) for more
information.
Other useful commands for config manipulation are
link:commands.html#config-unset[`:config-unset`] to reset a value to its default,
link:commands.html#config-clear[`:config-clear`] to reset the entire configuration,
and link:commands.html#config-cycle[`:config-cycle`] to cycle a setting between
different values.
Configuring qutebrowser via config.py
-------------------------------------
@ -77,6 +83,10 @@ configuration. Note that qutebrowser will never touch this file - this means
you'll be responsible for updating it when upgrading to a newer qutebrowser
version.
You can run `:config-edit` inside qutebrowser to open the file in your editor,
`:config-source` to reload the file (`:config-edit` does this automatically), or
`:config-write-py --defaults` to write a template file to work with.
The file should be located in the "config" location listed on
link:qute://version[], which is typically `~/.config/qutebrowser/config.py` on
Linux, `~/.qutebrowser/config.py` on macOS, and

View File

@ -280,11 +280,6 @@ Always restore open sites when qutebrowser is reopened.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[backend]]
@ -1304,11 +1299,6 @@ Move on to the next part when there's only one possible completion left.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[completion.scrollbar.padding]]
@ -1347,11 +1337,6 @@ Shrink the completion to be smaller than the configured size if there are no scr
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[completion.timestamp_format]]
@ -1395,11 +1380,6 @@ An application cache acts like an HTTP cache in some sense. For documents that u
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
@ -1448,11 +1428,6 @@ Note this option needs a restart with QtWebEngine on Qt < 5.9.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.default_encoding]]
@ -1471,11 +1446,6 @@ This needs to be enabled for `:inspector` to work and also adds an _Inspect_ ent
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -1486,11 +1456,6 @@ Try to pre-fetch DNS entries to speed up browsing.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
@ -1502,11 +1467,6 @@ This will flatten all the frames to become one scrollable page.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -1548,11 +1508,6 @@ When this is set to true, qutebrowser asks websites to not track your identity.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.headers.referer]]
@ -1586,11 +1541,6 @@ Whether host blocking is enabled.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.host_blocking.lists]]
@ -1633,11 +1583,6 @@ Enable or disable hyperlink auditing (`<a ping>`).
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content.images]]
@ -1646,11 +1591,6 @@ Whether images are automatically loaded in web pages.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.javascript.alert]]
@ -1659,11 +1599,6 @@ Show javascript alerts.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.javascript.can_access_clipboard]]
@ -1673,11 +1608,6 @@ With QtWebEngine, writing the clipboard as response to a user interaction is alw
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content.javascript.can_close_tabs]]
@ -1686,11 +1616,6 @@ Whether JavaScript can close tabs.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -1701,11 +1626,6 @@ Whether JavaScript can open new tabs without user interaction.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content.javascript.enabled]]
@ -1714,11 +1634,6 @@ Enables or disables JavaScript.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.javascript.log]]
@ -1742,11 +1657,6 @@ Use the standard JavaScript modal dialog for `alert()` and `confirm()`
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content.javascript.prompt]]
@ -1755,11 +1665,6 @@ Show javascript prompts.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.local_content_can_access_file_urls]]
@ -1768,11 +1673,6 @@ Whether locally loaded documents are allowed to access other local urls.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.local_content_can_access_remote_urls]]
@ -1781,11 +1681,6 @@ Whether locally loaded documents are allowed to access remote urls.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content.local_storage]]
@ -1794,11 +1689,6 @@ Whether support for HTML 5 local storage and Web SQL is enabled.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.media_capture]]
@ -1847,11 +1737,6 @@ Note that the files can still be downloaded by clicking the download button in t
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -1862,11 +1747,6 @@ Enables or disables plugins in Web pages.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content.print_element_backgrounds]]
@ -1875,11 +1755,6 @@ Whether the background color and images are also drawn when the page is printed.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
On QtWebEngine, this setting requires Qt 5.8 or newer.
@ -1890,11 +1765,6 @@ Open new windows in private browsing mode which does not record visited pages.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content.proxy]]
@ -1917,11 +1787,6 @@ Send DNS requests over the configured proxy.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
@ -1954,11 +1819,6 @@ Enables or disables WebGL.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content.xss_auditing]]
@ -1968,11 +1828,6 @@ Suspicious scripts will be blocked and reported in the inspector's JavaScript co
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[downloads.location.directory]]
@ -1991,11 +1846,6 @@ If set to false, `downloads.location.directory` will be used.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[downloads.location.remember]]
@ -2004,11 +1854,6 @@ Remember the last used download directory.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[downloads.location.suggestion]]
@ -2270,11 +2115,6 @@ This is needed for QtWebEngine to work with Nouveau drivers. This setting requir
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebEngine backend.
@ -2347,11 +2187,6 @@ Hide unmatched hints in rapid mode.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[hints.min_chars]]
@ -2412,11 +2247,6 @@ Ignored for number hints.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[hints.uppercase]]
@ -2425,11 +2255,6 @@ Make chars in hint strings uppercase.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[history_gap_interval]]
@ -2475,11 +2300,6 @@ Leave insert mode if a non-editable element is clicked.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[input.insert_mode.auto_load]]
@ -2488,11 +2308,6 @@ Automatically enter insert mode if an editable element is focused after loading
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[input.insert_mode.plugins]]
@ -2501,11 +2316,6 @@ Switch to insert mode when clicking flash and other plugins.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[input.links_included_in_focus_chain]]
@ -2514,11 +2324,6 @@ Include hyperlinks in the keyboard focus chain when tabbing.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[input.partial_timeout]]
@ -2537,11 +2342,6 @@ This disables the context menu.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[input.spatial_navigation]]
@ -2551,11 +2351,6 @@ Spatial navigation consists in the ability to navigate between focusable element
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[keyhint.blacklist]]
@ -2590,11 +2385,6 @@ Show messages in unfocused windows.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[new_instance_open_target]]
@ -2637,11 +2427,6 @@ Show a filebrowser in upload/download prompts.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[prompt.radius]]
@ -2668,11 +2453,6 @@ Show a scrollbar.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[scrolling.smooth]]
@ -2682,11 +2462,6 @@ Note smooth scrolling does not work with the `:scroll-px` command.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[session_default_name]]
@ -2704,11 +2479,6 @@ Hide the statusbar unless a message is shown.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[statusbar.padding]]
@ -2743,11 +2513,6 @@ Open new tabs (middleclick/ctrl+click) in the background.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[tabs.close_mouse_button]]
@ -2779,11 +2544,6 @@ Show favicons in the tab bar.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[tabs.indicator_padding]]
@ -2821,11 +2581,6 @@ Switch between tabs using the mouse wheel.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[tabs.new_position.related]]
@ -2929,11 +2684,6 @@ Open a new window for every tab.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[tabs.title.alignment]]
@ -3001,11 +2751,6 @@ Whether to wrap when changing tabs.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[url.auto_search]]
@ -3090,11 +2835,6 @@ Hide the window decoration when using wayland (requires restart)
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[window.title_format]]
@ -3164,11 +2904,6 @@ Whether the zoom factor on a frame applies only to the text or to all content.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.

View File

@ -223,6 +223,11 @@ class Config(QObject):
self._mutables = {}
self._yaml = yaml_config
def __iter__(self):
"""Iterate over Option, value tuples."""
for name, value in sorted(self._values.items()):
yield (self.get_opt(name), value)
def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly.
@ -365,10 +370,9 @@ class Config(QObject):
The changed config part as string.
"""
lines = []
for optname, value in sorted(self._values.items()):
opt = self.get_opt(optname)
for opt, value in self:
str_value = opt.typ.to_str(value)
lines.append('{} = {}'.format(optname, str_value))
lines.append('{} = {}'.format(opt.name, str_value))
if not lines:
lines = ['<Default configuration>']
return '\n'.join(lines)

View File

@ -27,7 +27,7 @@ from PyQt5.QtCore import QUrl
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.completion.models import configmodel
from qutebrowser.utils import objreg, utils, message, standarddir
from qutebrowser.config import configtypes, configexc, configfiles
from qutebrowser.config import configtypes, configexc, configfiles, configdata
from qutebrowser.misc import editor
@ -246,3 +246,35 @@ class ConfigCommands:
filename = os.path.join(standarddir.config(), 'config.py')
ed.edit_file(filename)
@cmdutils.register(instance='config-commands')
def config_write_py(self, filename=None, force=False, defaults=False):
"""Write the current configuration to a config.py file.
Args:
filename: The file to write to, or None for the default config.py.
force: Force overwriting existing files.
defaults: Write the defaults instead of values configured via :set.
"""
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
else:
filename = os.path.expanduser(filename)
if os.path.exists(filename) and not force:
raise cmdexc.CommandError("{} already exists - use --force to "
"overwrite!".format(filename))
if defaults:
options = [(opt, opt.default)
for _name, opt in sorted(configdata.DATA.items())]
bindings = dict(configdata.DATA['bindings.default'].default)
commented = True
else:
options = list(self._config)
bindings = dict(self._config.get_obj('bindings.commands'))
commented = False
writer = configfiles.ConfigPyWriter(options, bindings,
commented=commented)
writer.write(filename)

View File

@ -245,6 +245,101 @@ class ConfigAPI:
self._keyconfig.unbind(key, mode=mode)
class ConfigPyWriter:
"""Writer for config.py files from given settings."""
def __init__(self, options, bindings, *, commented):
self._options = options
self._bindings = bindings
self._commented = commented
def write(self, filename):
"""Write the config to the given file."""
with open(filename, 'w', encoding='utf-8') as f:
f.write('\n'.join(self._gen_lines()))
def _line(self, line):
"""Get an (optionally commented) line."""
if self._commented:
if line.startswith('#'):
return '#' + line
else:
return '# ' + line
else:
return line
def _gen_lines(self):
"""Generate a config.py with the given settings/bindings.
Yields individual lines.
"""
yield from self._gen_header()
yield from self._gen_options()
yield from self._gen_bindings()
def _gen_header(self):
"""Generate the initial header of the config."""
yield self._line("# Autogenerated config.py")
yield self._line("# Documentation:")
yield self._line("# qute://help/configuring.html")
yield self._line("# qute://help/settings.html")
yield ''
if self._commented:
# When generated from an autoconfig.yml with commented=False,
# we don't want to load that autoconfig.yml anymore.
yield self._line("# This is here so configs done via the GUI are "
"still loaded.")
yield self._line("# Remove it to not load settings done via the "
"GUI.")
yield self._line("config.load_autoconfig()")
yield ''
else:
yield self._line("# Uncomment this to still load settings "
"configured via autoconfig.yml")
yield self._line("# config.load_autoconfig()")
yield ''
def _gen_options(self):
"""Generate the options part of the config."""
for opt, value in self._options:
if opt.name in ['bindings.commands', 'bindings.default']:
continue
for line in textwrap.wrap(opt.description):
yield self._line("# {}".format(line))
yield self._line("# Type: {}".format(opt.typ.get_name()))
valid_values = opt.typ.get_valid_values()
if valid_values is not None and valid_values.generate_docs:
yield self._line("# Valid values:")
for val in valid_values:
try:
desc = valid_values.descriptions[val]
yield self._line("# - {}: {}".format(val, desc))
except KeyError:
yield self._line("# - {}".format(val))
yield self._line('c.{} = {!r}'.format(opt.name, value))
yield ''
def _gen_bindings(self):
"""Generate the bindings part of the config."""
normal_bindings = self._bindings.pop('normal', {})
if normal_bindings:
yield self._line('# Bindings for normal mode')
for key, command in sorted(normal_bindings.items()):
yield self._line('config.bind({!r}, {!r})'.format(key, command))
for mode, mode_bindings in sorted(self._bindings.items()):
yield ''
yield self._line('# Bindings for {} mode'.format(mode))
for key, command in sorted(mode_bindings.items()):
yield self._line('config.bind({!r}, {!r}, mode={!r})'.format(
key, command, mode))
def read_config_py(filename, raising=False):
"""Read a config.py file.
@ -253,6 +348,9 @@ def read_config_py(filename, raising=False):
raising: Raise exceptions happening in config.py.
This is needed during tests to use pytest's inspection.
"""
assert config.instance is not None
assert config.key_instance is not None
api = ConfigAPI(config.instance, config.key_instance)
container = config.ConfigContainer(config.instance, configapi=api)
basename = os.path.basename(filename)

View File

@ -78,13 +78,15 @@ class ValidValues:
Attributes:
values: A list with the allowed untransformed values.
descriptions: A dict with value/desc mappings.
generate_docs: Whether to show the values in the docs.
"""
def __init__(self, *values):
def __init__(self, *values, generate_docs=True):
if not values:
raise ValueError("ValidValues with no values makes no sense!")
self.descriptions = {}
self.values = []
self.generate_docs = generate_docs
for value in values:
if isinstance(value, str):
# Value without description
@ -608,7 +610,7 @@ class Bool(BaseType):
def __init__(self, none_ok=False):
super().__init__(none_ok)
self.valid_values = ValidValues('true', 'false')
self.valid_values = ValidValues('true', 'false', generate_docs=False)
def to_py(self, value):
self._basic_py_validation(value, bool)

View File

@ -421,7 +421,7 @@ def _generate_setting_option(f, opt):
f.write("\n")
valid_values = opt.typ.get_valid_values()
if valid_values is not None:
if valid_values is not None and valid_values.generate_docs:
f.write("Valid values:\n")
f.write("\n")
for val in valid_values:

View File

@ -1,29 +0,0 @@
# 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

@ -93,11 +93,6 @@ class TestChangeFilter:
class TestKeyConfig:
@pytest.fixture
def keyconf(self, config_stub):
config_stub.val.aliases = {}
return config.KeyConfig(config_stub)
@pytest.fixture
def no_bindings(self):
"""Get a dict with no bindings."""
@ -107,14 +102,14 @@ class TestKeyConfig:
('A', 'A'),
('<Ctrl-X>', '<ctrl+x>'),
])
def test_prepare_valid(self, keyconf, key, expected):
def test_prepare_valid(self, key_config_stub, key, expected):
"""Make sure prepare normalizes the key."""
assert keyconf._prepare(key, 'normal') == expected
assert key_config_stub._prepare(key, 'normal') == expected
def test_prepare_invalid(self, keyconf):
def test_prepare_invalid(self, key_config_stub):
"""Make sure prepare checks the mode."""
with pytest.raises(configexc.KeybindingError):
assert keyconf._prepare('x', 'abnormal')
assert key_config_stub._prepare('x', 'abnormal')
@pytest.mark.parametrize('commands, expected', [
# Unbinding default key
@ -126,7 +121,8 @@ class TestKeyConfig:
# Unbinding unknown key
({'x': None}, {'a': 'message-info foo', 'b': 'message-info bar'}),
])
def test_get_bindings_for_and_get_command(self, keyconf, config_stub,
def test_get_bindings_for_and_get_command(self, key_config_stub,
config_stub,
commands, expected):
orig_default_bindings = {'normal': {'a': 'message-info foo',
'b': 'message-info bar'},
@ -139,18 +135,19 @@ class TestKeyConfig:
'register': {}}
config_stub.val.bindings.default = copy.deepcopy(orig_default_bindings)
config_stub.val.bindings.commands = {'normal': commands}
bindings = keyconf.get_bindings_for('normal')
bindings = key_config_stub.get_bindings_for('normal')
# Make sure the code creates a copy and doesn't modify the setting
assert config_stub.val.bindings.default == orig_default_bindings
assert bindings == expected
for key, command in expected.items():
assert keyconf.get_command(key, 'normal') == command
assert key_config_stub.get_command(key, 'normal') == command
def test_get_command_unbound(self, keyconf, config_stub, no_bindings):
def test_get_command_unbound(self, key_config_stub, config_stub,
no_bindings):
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
assert keyconf.get_command('foobar', 'normal') is None
assert key_config_stub.get_command('foobar', 'normal') is None
@pytest.mark.parametrize('bindings, expected', [
# Simple
@ -166,46 +163,47 @@ class TestKeyConfig:
({'a': 'message-info foo ;; message-info bar'},
{'message-info foo': ['a'], 'message-info bar': ['a']}),
])
def test_get_reverse_bindings_for(self, keyconf, config_stub, no_bindings,
bindings, expected):
def test_get_reverse_bindings_for(self, key_config_stub, config_stub,
no_bindings, bindings, expected):
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = {'normal': bindings}
assert keyconf.get_reverse_bindings_for('normal') == expected
assert key_config_stub.get_reverse_bindings_for('normal') == expected
@pytest.mark.parametrize('key', ['a', '<Ctrl-X>', 'b'])
def test_bind_duplicate(self, keyconf, config_stub, key):
def test_bind_duplicate(self, key_config_stub, config_stub, key):
config_stub.val.bindings.default = {'normal': {'a': 'nop',
'<Ctrl+x>': 'nop'}}
config_stub.val.bindings.commands = {'normal': {'b': 'nop'}}
keyconf.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'message-info foo'
key_config_stub.bind(key, 'message-info foo', mode='normal')
assert key_config_stub.get_command(key, 'normal') == 'message-info foo'
@pytest.mark.parametrize('mode', ['normal', 'caret'])
@pytest.mark.parametrize('command', [
'message-info foo',
'nop ;; wq', # https://github.com/qutebrowser/qutebrowser/issues/3002
])
def test_bind(self, keyconf, config_stub, qtbot, no_bindings,
def test_bind(self, key_config_stub, config_stub, qtbot, no_bindings,
mode, command):
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
with qtbot.wait_signal(config_stub.changed):
keyconf.bind('a', command, mode=mode)
key_config_stub.bind('a', command, mode=mode)
assert config_stub.val.bindings.commands[mode]['a'] == command
assert keyconf.get_bindings_for(mode)['a'] == command
assert keyconf.get_command('a', mode) == command
assert key_config_stub.get_bindings_for(mode)['a'] == command
assert key_config_stub.get_command('a', mode) == command
def test_bind_mode_changing(self, keyconf, config_stub, no_bindings):
def test_bind_mode_changing(self, key_config_stub, config_stub,
no_bindings):
"""Make sure we can bind to a command which changes the mode.
https://github.com/qutebrowser/qutebrowser/issues/2989
"""
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
keyconf.bind('a', 'set-cmd-text :nop ;; rl-beginning-of-line',
mode='normal')
key_config_stub.bind('a', 'set-cmd-text :nop ;; rl-beginning-of-line',
mode='normal')
@pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings
@ -213,7 +211,8 @@ class TestKeyConfig:
('<Ctrl-X>', '<ctrl+x>')
])
@pytest.mark.parametrize('mode', ['normal', 'caret', 'prompt'])
def test_unbind(self, keyconf, config_stub, qtbot, key, normalized, mode):
def test_unbind(self, key_config_stub, config_stub, qtbot,
key, normalized, mode):
default_bindings = {
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
@ -228,9 +227,9 @@ class TestKeyConfig:
}
with qtbot.wait_signal(config_stub.changed):
keyconf.unbind(key, mode=mode)
key_config_stub.unbind(key, mode=mode)
assert keyconf.get_command(key, mode) is None
assert key_config_stub.get_command(key, mode) is None
mode_bindings = config_stub.val.bindings.commands[mode]
if key == 'b' and mode != 'prompt':
@ -241,13 +240,13 @@ class TestKeyConfig:
assert default_bindings[mode] == old_default_bindings[mode]
assert mode_bindings[normalized] is None
def test_unbind_unbound(self, keyconf, config_stub, no_bindings):
def test_unbind_unbound(self, key_config_stub, config_stub, no_bindings):
"""Try unbinding a key which is not bound."""
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
with pytest.raises(configexc.KeybindingError,
match="Can't find binding 'foobar' in normal mode"):
keyconf.unbind('foobar', mode='normal')
key_config_stub.unbind('foobar', mode='normal')
class TestConfig:

View File

@ -31,8 +31,8 @@ from qutebrowser.misc import objects
@pytest.fixture
def commands(config_stub, keyconf):
return configcommands.ConfigCommands(config_stub, keyconf)
def commands(config_stub, key_config_stub):
return configcommands.ConfigCommands(config_stub, key_config_stub)
class TestSet:
@ -263,7 +263,8 @@ class TestSource:
"""Test :config-source."""
pytestmark = pytest.mark.usefixtures('config_tmpdir', 'data_tmpdir')
pytestmark = pytest.mark.usefixtures('config_tmpdir', 'data_tmpdir',
'config_stub', 'key_config_stub')
@pytest.mark.parametrize('use_default_dir', [True, False])
@pytest.mark.parametrize('clear', [True, False])
@ -302,14 +303,17 @@ class TestEdit:
"""Tests for :config-edit."""
def test_no_source(self, commands, mocker, config_tmpdir):
pytestmark = pytest.mark.usefixtures('config_tmpdir', 'data_tmpdir',
'config_stub', 'key_config_stub')
def test_no_source(self, commands, mocker):
mock = mocker.patch('qutebrowser.config.configcommands.editor.'
'ExternalEditor._start_editor', autospec=True)
commands.config_edit(no_source=True)
mock.assert_called_once_with(unittest.mock.ANY)
@pytest.fixture
def patch_editor(self, mocker, config_tmpdir, data_tmpdir):
def patch_editor(self, mocker):
"""Write a config.py file."""
def do_patch(text):
def _write_file(editor_self):
@ -345,6 +349,55 @@ class TestEdit:
assert msg.text == expected
class TestWritePy:
"""Tests for :config-write-py."""
def test_custom(self, commands, config_stub, key_config_stub, tmpdir):
confpy = tmpdir / 'config.py'
config_stub.val.content.javascript.enabled = True
key_config_stub.bind(',x', 'message-info foo', mode='normal')
commands.config_write_py(str(confpy))
lines = confpy.read_text('utf-8').splitlines()
assert "c.content.javascript.enabled = True" in lines
assert "config.bind(',x', 'message-info foo')" in lines
def test_defaults(self, commands, tmpdir):
confpy = tmpdir / 'config.py'
commands.config_write_py(str(confpy), defaults=True)
lines = confpy.read_text('utf-8').splitlines()
assert "# c.content.javascript.enabled = True" in lines
assert "# config.bind('H', 'back')" in lines
def test_default_location(self, commands, config_tmpdir):
confpy = config_tmpdir / 'config.py'
commands.config_write_py()
lines = confpy.read_text('utf-8').splitlines()
assert '# Autogenerated config.py' in lines
def test_existing_file(self, commands, tmpdir):
confpy = tmpdir / 'config.py'
confpy.ensure()
with pytest.raises(cmdexc.CommandError) as excinfo:
commands.config_write_py(str(confpy))
expected = " already exists - use --force to overwrite!"
assert str(excinfo.value).endswith(expected)
def test_existing_file_force(self, commands, tmpdir):
confpy = tmpdir / 'config.py'
confpy.ensure()
commands.config_write_py(str(confpy), force=True)
lines = confpy.read_text('utf-8').splitlines()
assert '# Autogenerated config.py' in lines
class TestBind:
"""Tests for :bind and :unbind."""
@ -355,14 +408,15 @@ class TestBind:
return {'normal': {}}
@pytest.mark.parametrize('command', ['nop', 'nope'])
def test_bind(self, commands, config_stub, no_bindings, keyconf, command):
def test_bind(self, commands, config_stub, no_bindings, key_config_stub,
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
assert key_config_stub.get_command('a', 'normal') == command
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
assert yaml_bindings['a'] == command
@ -413,7 +467,7 @@ class TestBind:
commands.bind('a', 'nop', mode='wrongmode')
@pytest.mark.parametrize('key', ['a', 'b', '<Ctrl-X>'])
def test_bind_duplicate(self, commands, config_stub, keyconf, key):
def test_bind_duplicate(self, commands, config_stub, key_config_stub, key):
"""Run ':bind' with a key which already has been bound.'.
Also tests for https://github.com/qutebrowser/qutebrowser/issues/1544
@ -426,7 +480,7 @@ class TestBind:
}
commands.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'message-info foo'
assert key_config_stub.get_command(key, 'normal') == 'message-info foo'
def test_bind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
@ -442,7 +496,8 @@ class TestBind:
('c', 'c'), # :bind then :unbind
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
])
def test_unbind(self, commands, keyconf, config_stub, key, normalized):
def test_unbind(self, commands, key_config_stub, config_stub,
key, normalized):
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
@ -456,7 +511,7 @@ class TestBind:
commands.bind(key, 'nop')
commands.unbind(key)
assert keyconf.get_command(key, 'normal') is None
assert key_config_stub.get_command(key, 'normal') is None
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
if key in 'bc':

View File

@ -21,11 +21,13 @@
import os
import sys
import unittest.mock
import textwrap
import pytest
from qutebrowser.config import config, configfiles, configexc, configdata
from qutebrowser.utils import utils
from qutebrowser.config import (config, configfiles, configexc, configdata,
configtypes)
from qutebrowser.utils import utils, usertypes
from PyQt5.QtCore import QSettings
@ -271,14 +273,15 @@ class ConfPy:
'qbmodule.run(config)')
@pytest.fixture
def confpy(tmpdir, config_tmpdir, data_tmpdir, config_stub, key_config_stub):
return ConfPy(tmpdir)
class TestConfigPyModules:
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
@pytest.fixture
def confpy(self, tmpdir, config_tmpdir, data_tmpdir):
return ConfPy(tmpdir)
@pytest.fixture
def qbmodulepy(self, tmpdir):
return ConfPy(tmpdir, filename="qbmodule.py")
@ -340,10 +343,6 @@ class TestConfigPy:
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
@pytest.fixture
def confpy(self, tmpdir, config_tmpdir, data_tmpdir):
return ConfPy(tmpdir)
def test_assertions(self, confpy):
"""Make sure assertions in config.py work for these tests."""
confpy.write('assert False')
@ -527,6 +526,153 @@ class TestConfigPy:
assert error.traceback is not None
class TestConfigPyWriter:
def test_output(self):
desc = ("This is an option description.\n\n"
"Nullam eu ante vel est convallis dignissim. Fusce suscipit, "
"wisi nec facilisis facilisis, est dui fermentum leo, quis "
"tempor ligula erat quis odio.")
opt = configdata.Option(
name='opt', typ=configtypes.Int(), default='def',
backends=[usertypes.Backend.QtWebEngine], raw_backends=None,
description=desc)
options = [(opt, 'val')]
bindings = {'normal': {',x': 'message-info normal'},
'caret': {',y': 'message-info caret'}}
writer = configfiles.ConfigPyWriter(options, bindings, commented=False)
text = '\n'.join(writer._gen_lines())
assert text == textwrap.dedent("""
# Autogenerated config.py
# Documentation:
# qute://help/configuring.html
# qute://help/settings.html
# Uncomment this to still load settings configured via autoconfig.yml
# config.load_autoconfig()
# This is an option description. Nullam eu ante vel est convallis
# dignissim. Fusce suscipit, wisi nec facilisis facilisis, est dui
# fermentum leo, quis tempor ligula erat quis odio.
# Type: Int
c.opt = 'val'
# Bindings for normal mode
config.bind(',x', 'message-info normal')
# Bindings for caret mode
config.bind(',y', 'message-info caret', mode='caret')
""").strip()
def test_binding_options_hidden(self):
opt1 = configdata.DATA['bindings.default']
opt2 = configdata.DATA['bindings.commands']
options = [(opt1, {'normal': {'x': 'message-info x'}}),
(opt2, {})]
writer = configfiles.ConfigPyWriter(options, bindings={},
commented=False)
text = '\n'.join(writer._gen_lines())
assert 'bindings.default' not in text
assert 'bindings.commands' not in text
def test_commented(self):
opt = configdata.Option(
name='opt', typ=configtypes.Int(), default='def',
backends=[usertypes.Backend.QtWebEngine], raw_backends=None,
description='Hello World')
options = [(opt, 'val')]
bindings = {'normal': {',x': 'message-info normal'},
'caret': {',y': 'message-info caret'}}
writer = configfiles.ConfigPyWriter(options, bindings, commented=True)
lines = list(writer._gen_lines())
assert "## Autogenerated config.py" in lines
assert "# config.load_autoconfig()" in lines
assert "# c.opt = 'val'" in lines
assert "## Bindings for normal mode" in lines
assert "# config.bind(',x', 'message-info normal')" in lines
caret_bind = ("# config.bind(',y', 'message-info caret', "
"mode='caret')")
assert caret_bind in lines
def test_valid_values(self):
opt1 = configdata.Option(
name='opt1', typ=configtypes.BoolAsk(), default='ask',
backends=[usertypes.Backend.QtWebEngine], raw_backends=None,
description='Hello World')
opt2 = configdata.Option(
name='opt2', typ=configtypes.ColorSystem(), default='rgb',
backends=[usertypes.Backend.QtWebEngine], raw_backends=None,
description='All colors are beautiful!')
options = [(opt1, 'ask'), (opt2, 'rgb')]
writer = configfiles.ConfigPyWriter(options, bindings={},
commented=False)
text = '\n'.join(writer._gen_lines())
expected = textwrap.dedent("""
# Hello World
# Type: BoolAsk
# Valid values:
# - true
# - false
# - ask
c.opt1 = 'ask'
# All colors are beautiful!
# Type: ColorSystem
# Valid values:
# - rgb: Interpolate in the RGB color system.
# - hsv: Interpolate in the HSV color system.
# - hsl: Interpolate in the HSL color system.
# - none: Don't show a gradient.
c.opt2 = 'rgb'
""")
assert expected in text
def test_empty(self):
writer = configfiles.ConfigPyWriter(options=[], bindings={},
commented=False)
text = '\n'.join(writer._gen_lines())
expected = textwrap.dedent("""
# Autogenerated config.py
# Documentation:
# qute://help/configuring.html
# qute://help/settings.html
# Uncomment this to still load settings configured via autoconfig.yml
# config.load_autoconfig()
""").lstrip()
assert text == expected
def test_write(self, tmpdir):
pyfile = tmpdir / 'config.py'
writer = configfiles.ConfigPyWriter(options=[], bindings={},
commented=False)
writer.write(str(pyfile))
lines = pyfile.read_text('utf-8').splitlines()
assert '# Autogenerated config.py' in lines
def test_defaults_work(self, confpy):
"""Get a config.py with default values and run it."""
options = [(opt, opt.default)
for _name, opt in sorted(configdata.DATA.items())]
bindings = dict(configdata.DATA['bindings.default'].default)
writer = configfiles.ConfigPyWriter(options, bindings, commented=False)
writer.write(confpy.filename)
try:
configfiles.read_config_py(confpy.filename)
except configexc.ConfigFileErrors as exc:
# Make sure no other errors happened
for error in exc.errors:
assert isinstance(error.exception, configexc.BackendError)
@pytest.fixture
def init_patch(qapp, fake_save_manager, config_tmpdir, data_tmpdir,
config_stub, monkeypatch):