From 7fc5e42ccae0b19ee41de90458c4d6f13ff92fc4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 4 Oct 2017 11:14:24 +0200 Subject: [PATCH 01/13] Initial :config-write-py implementation --- qutebrowser/config/config.py | 10 +++++++--- qutebrowser/config/configcommands.py | 30 +++++++++++++++++++++++++++- qutebrowser/config/configfiles.py | 22 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index d6dbd1e86..ffd0780d9 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -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 = [''] return '\n'.join(lines) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 0eb93387e..5249ca778 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -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,31 @@ 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) + else: + options = list(self._config) + bindings = dict(self._config.get_obj('bindings.commands')) + + configfiles.write_config_py(filename, options, bindings) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index e1c6f720c..fe55312f1 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -245,6 +245,28 @@ class ConfigAPI: self._keyconfig.unbind(key, mode=mode) +def write_config_py(filename, options, bindings): + """Write a config.py file from given values.""" + with open(filename, 'w', encoding='utf-8') as f: + for opt, value in options: + if opt.name in ['bindings.commands', 'bindings.default']: + continue + for line in textwrap.wrap(opt.description): + f.write('# {}\n'.format(line)) + f.write('c.{} = {!r}\n\n'.format(opt.name, value)) + + normal_bindings = bindings.pop('normal', {}) + f.write('# Bindings for normal mode\n') + for key, command in sorted(normal_bindings.items()): + f.write('config.bind({!r}, {!r})\n'.format(key, command)) + + for mode, mode_bindings in sorted(bindings.items()): + f.write('\n# Bindings for {} mode\n'.format(mode)) + for key, command in sorted(mode_bindings.items()): + f.write('config.bind({!r}, {!r}, mode={!r})\n'.format( + key, command, mode)) + + def read_config_py(filename, raising=False): """Read a config.py file. From 6fa001481d544df4a215fe4e0b4fe13d30c90447 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 4 Oct 2017 11:43:14 +0200 Subject: [PATCH 02/13] Add some comments to generated config.py --- qutebrowser/config/configfiles.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index fe55312f1..8f174b869 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -248,11 +248,31 @@ class ConfigAPI: def write_config_py(filename, options, bindings): """Write a config.py file from given values.""" with open(filename, 'w', encoding='utf-8') as f: + f.write("# Autogenerated config.py - please remove lines you don't " + "customize!\n") + f.write("# Documentation:\n") + f.write("# qute://help/configuring.html\n") + f.write("# qute://help/settings.html\n\n") + for opt, value in options: if opt.name in ['bindings.commands', 'bindings.default']: continue + for line in textwrap.wrap(opt.description): f.write('# {}\n'.format(line)) + + f.write('# Type: {}\n'.format(opt.typ.get_name())) + + valid_values = opt.typ.get_valid_values() + if valid_values is not None: + f.write("# Valid values:\n") + for val in valid_values: + try: + desc = valid_values.descriptions[val] + f.write("# - {}: {}".format(val, desc) + "\n") + except KeyError: + f.write("# - {}".format(val) + "\n") + f.write('c.{} = {!r}\n\n'.format(opt.name, value)) normal_bindings = bindings.pop('normal', {}) From 36857d9250cf28c3560d25dd9b71bdf3ac95b57b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 4 Oct 2017 11:46:42 +0200 Subject: [PATCH 03/13] Remove valid values for Bool from docs --- doc/help/settings.asciidoc | 265 ------------------------------ qutebrowser/config/configfiles.py | 2 +- qutebrowser/config/configtypes.py | 6 +- scripts/dev/src2asciidoc.py | 2 +- 4 files changed, 6 insertions(+), 269 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 7aee1d66e..f65f42fbc 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -280,11 +280,6 @@ Always restore open sites when qutebrowser is reopened. Type: <> -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: <> -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: <> -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: <> -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: <> -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: <> -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: <> -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: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[content.headers.referer]] @@ -1586,11 +1541,6 @@ Whether host blocking is enabled. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[content.host_blocking.lists]] @@ -1633,11 +1583,6 @@ Enable or disable hyperlink auditing (``). Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[content.images]] @@ -1646,11 +1591,6 @@ Whether images are automatically loaded in web pages. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[content.javascript.alert]] @@ -1659,11 +1599,6 @@ Show javascript alerts. Type: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[content.javascript.can_close_tabs]] @@ -1686,11 +1616,6 @@ Whether JavaScript can close tabs. Type: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[content.javascript.enabled]] @@ -1714,11 +1634,6 @@ Enables or disables JavaScript. Type: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[content.javascript.prompt]] @@ -1755,11 +1665,6 @@ Show javascript prompts. Type: <> -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: <> -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: <> -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: <> -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: <> -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: <> -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: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[content.proxy]] @@ -1917,11 +1787,6 @@ Send DNS requests over the configured proxy. Type: <> -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: <> -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: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[downloads.location.remember]] @@ -2004,11 +1854,6 @@ Remember the last used download directory. Type: <> -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: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[hints.min_chars]] @@ -2412,11 +2247,6 @@ Ignored for number hints. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[hints.uppercase]] @@ -2425,11 +2255,6 @@ Make chars in hint strings uppercase. Type: <> -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: <> -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: <> -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: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[input.partial_timeout]] @@ -2537,11 +2342,6 @@ This disables the context menu. Type: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[keyhint.blacklist]] @@ -2590,11 +2385,6 @@ Show messages in unfocused windows. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[new_instance_open_target]] @@ -2637,11 +2427,6 @@ Show a filebrowser in upload/download prompts. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[prompt.radius]] @@ -2668,11 +2453,6 @@ Show a scrollbar. Type: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[session_default_name]] @@ -2704,11 +2479,6 @@ Hide the statusbar unless a message is shown. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[statusbar.padding]] @@ -2743,11 +2513,6 @@ Open new tabs (middleclick/ctrl+click) in the background. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[tabs.close_mouse_button]] @@ -2779,11 +2544,6 @@ Show favicons in the tab bar. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[tabs.indicator_padding]] @@ -2821,11 +2581,6 @@ Switch between tabs using the mouse wheel. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[tabs.new_position.related]] @@ -2929,11 +2684,6 @@ Open a new window for every tab. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ [[tabs.title.alignment]] @@ -3001,11 +2751,6 @@ Whether to wrap when changing tabs. Type: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[true]+ [[url.auto_search]] @@ -3090,11 +2835,6 @@ Hide the window decoration when using wayland (requires restart) Type: <> -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: <> -Valid values: - - * +true+ - * +false+ - Default: +pass:[false]+ This setting is only available with the QtWebKit backend. diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 8f174b869..c23e616a0 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -264,7 +264,7 @@ def write_config_py(filename, options, bindings): f.write('# Type: {}\n'.format(opt.typ.get_name())) 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") for val in valid_values: try: diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index afe9eb372..aac320cbb 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -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) diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index f12d6601b..8ea6b7c30 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -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: From 9257538dcfa36f6f440182f62e21aa3fb160a4eb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 4 Oct 2017 11:48:46 +0200 Subject: [PATCH 04/13] Regenerate docs --- doc/help/commands.asciidoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 6083d1e3a..349a67a5b 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -36,6 +36,7 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Open the config.py file in the editor. |<>|Read a config.py file. |<>|Unset an option. +|<>|Write the current configuration to a config.py file. |<>|Download a given URL, or current page if no URL given. |<>|Cancel the last/[count]th download. |<>|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']+ From 8dc34cf78afd85eb853446bdda0ad9a084184a24 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 08:38:35 +0200 Subject: [PATCH 05/13] Write commented lines with :write-config-py when writing defaults --- qutebrowser/config/configcommands.py | 5 ++- qutebrowser/config/configfiles.py | 46 ++++++++++++++++++---------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 5249ca778..6c2880100 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -269,8 +269,11 @@ class ConfigCommands: 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 - configfiles.write_config_py(filename, options, bindings) + configfiles.write_config_py(filename, options, bindings, + commented=commented) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index c23e616a0..0aa61c149 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -245,45 +245,59 @@ class ConfigAPI: self._keyconfig.unbind(key, mode=mode) -def write_config_py(filename, options, bindings): +def write_config_py(filename, options, bindings, *, commented): """Write a config.py file from given values.""" + c = '#' if commented else '' + cs = '# ' if commented else '' with open(filename, 'w', encoding='utf-8') as f: - f.write("# Autogenerated config.py - please remove lines you don't " - "customize!\n") - f.write("# Documentation:\n") - f.write("# qute://help/configuring.html\n") - f.write("# qute://help/settings.html\n\n") + f.write(c + "# Autogenerated config.py\n") + f.write(c + "# Documentation:\n") + f.write(c + "# qute://help/configuring.html\n") + f.write(c + "# qute://help/settings.html\n\n") + if commented: + # When generated from an autoconfig.yml with commented=False, + # we don't want to load that autoconfig.yml anymore. + f.write("## This is here so configs done via the GUI are still " + "loaded.\n") + f.write("## Remove it to not load settings done via the GUI.\n") + f.write("config.load_autoconfig()\n\n") + else: + f.write("# Uncomment this to still load settings configured via " + "autoconfig.yml\n") + f.write("# config.load_autoconfig()\n\n") for opt, value in options: if opt.name in ['bindings.commands', 'bindings.default']: continue for line in textwrap.wrap(opt.description): - f.write('# {}\n'.format(line)) + f.write(c + "# {}\n".format(line)) - f.write('# Type: {}\n'.format(opt.typ.get_name())) + f.write(c + "# Type: {}\n".format(opt.typ.get_name())) valid_values = opt.typ.get_valid_values() if valid_values is not None and valid_values.generate_docs: - f.write("# Valid values:\n") + f.write(c + "# Valid values:\n") for val in valid_values: try: desc = valid_values.descriptions[val] - f.write("# - {}: {}".format(val, desc) + "\n") + f.write(c + "# - {}: {}".format(val, desc) + "\n") except KeyError: - f.write("# - {}".format(val) + "\n") + f.write(c + "# - {}".format(val) + "\n") - f.write('c.{} = {!r}\n\n'.format(opt.name, value)) + f.write(cs + 'c.{} = {!r}\n\n'.format(opt.name, value)) normal_bindings = bindings.pop('normal', {}) - f.write('# Bindings for normal mode\n') + if normal_bindings: + f.write(c + '# Bindings for normal mode\n') for key, command in sorted(normal_bindings.items()): - f.write('config.bind({!r}, {!r})\n'.format(key, command)) + f.write(cs + 'config.bind({!r}, {!r})\n'.format(key, command)) for mode, mode_bindings in sorted(bindings.items()): - f.write('\n# Bindings for {} mode\n'.format(mode)) + f.write('\n') + f.write(c + '# Bindings for {} mode\n'.format(mode)) for key, command in sorted(mode_bindings.items()): - f.write('config.bind({!r}, {!r}, mode={!r})\n'.format( + f.write(cs + 'config.bind({!r}, {!r}, mode={!r})\n'.format( key, command, mode)) From 231b0522ca46a781e63d3ac151ec8a5ac4db7bb1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 09:01:33 +0200 Subject: [PATCH 06/13] Refactor write_config_py --- qutebrowser/config/configcommands.py | 5 +- qutebrowser/config/configfiles.py | 97 +++++++++++++++++++--------- 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 6c2880100..a97e8bc78 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -275,5 +275,6 @@ class ConfigCommands: bindings = dict(self._config.get_obj('bindings.commands')) commented = False - configfiles.write_config_py(filename, options, bindings, - commented=commented) + writer = configfiles.ConfigPyWriter(options, bindings, + commented=commented) + writer.write(filename) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 0aa61c149..056633ca7 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -245,59 +245,94 @@ class ConfigAPI: self._keyconfig.unbind(key, mode=mode) -def write_config_py(filename, options, bindings, *, commented): - """Write a config.py file from given values.""" - c = '#' if commented else '' - cs = '# ' if commented else '' - with open(filename, 'w', encoding='utf-8') as f: - f.write(c + "# Autogenerated config.py\n") - f.write(c + "# Documentation:\n") - f.write(c + "# qute://help/configuring.html\n") - f.write(c + "# qute://help/settings.html\n\n") - if commented: +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_text())) + + 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_text(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 intiial 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\n") + if self._commented: # When generated from an autoconfig.yml with commented=False, # we don't want to load that autoconfig.yml anymore. - f.write("## This is here so configs done via the GUI are still " - "loaded.\n") - f.write("## Remove it to not load settings done via the GUI.\n") - f.write("config.load_autoconfig()\n\n") + 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()\n") else: - f.write("# Uncomment this to still load settings configured via " - "autoconfig.yml\n") - f.write("# config.load_autoconfig()\n\n") + yield self._line("# Uncomment this to still load settings " + "configured via autoconfig.yml") + yield self._line("# config.load_autoconfig()\n") - for opt, value in options: + 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): - f.write(c + "# {}\n".format(line)) + yield self._line("# {}".format(line)) - f.write(c + "# Type: {}\n".format(opt.typ.get_name())) + 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: - f.write(c + "# Valid values:\n") + yield self._line("# Valid values:") for val in valid_values: try: desc = valid_values.descriptions[val] - f.write(c + "# - {}: {}".format(val, desc) + "\n") + yield self._line("# - {}: {}".format(val, desc)) except KeyError: - f.write(c + "# - {}".format(val) + "\n") + yield self._line("# - {}".format(val)) - f.write(cs + 'c.{} = {!r}\n\n'.format(opt.name, value)) + yield self._line('c.{} = {!r}\n'.format(opt.name, value)) - normal_bindings = bindings.pop('normal', {}) + def _gen_bindings(self): + """Generate the bindings part of the config.""" + normal_bindings = self._bindings.pop('normal', {}) if normal_bindings: - f.write(c + '# Bindings for normal mode\n') + yield self._line('# Bindings for normal mode') for key, command in sorted(normal_bindings.items()): - f.write(cs + 'config.bind({!r}, {!r})\n'.format(key, command)) + yield self._line('config.bind({!r}, {!r})'.format(key, command)) - for mode, mode_bindings in sorted(bindings.items()): - f.write('\n') - f.write(c + '# Bindings for {} mode\n'.format(mode)) + 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()): - f.write(cs + 'config.bind({!r}, {!r}, mode={!r})\n'.format( + yield self._line('config.bind({!r}, {!r}, mode={!r})'.format( key, command, mode)) From ade0e1bd0be2ebde4eecf96f520c10d4f74f8d0c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 10:21:31 +0200 Subject: [PATCH 07/13] Finish ConfigPyWriter and tests --- qutebrowser/config/configfiles.py | 18 ++-- tests/unit/config/test_configfiles.py | 138 +++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 056633ca7..aab210bf1 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -257,7 +257,7 @@ class ConfigPyWriter: 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_text())) + f.write('\n'.join(self._gen_lines())) def _line(self, line): """Get an (optionally commented) line.""" @@ -269,7 +269,7 @@ class ConfigPyWriter: else: return line - def _gen_text(self): + def _gen_lines(self): """Generate a config.py with the given settings/bindings. Yields individual lines. @@ -279,11 +279,12 @@ class ConfigPyWriter: yield from self._gen_bindings() def _gen_header(self): - """Generate the intiial header of the config.""" + """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\n") + 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. @@ -291,11 +292,13 @@ class ConfigPyWriter: "still loaded.") yield self._line("# Remove it to not load settings done via the " "GUI.") - yield self._line("config.load_autoconfig()\n") + 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()\n") + yield self._line("# config.load_autoconfig()") + yield '' def _gen_options(self): """Generate the options part of the config.""" @@ -318,7 +321,8 @@ class ConfigPyWriter: except KeyError: yield self._line("# - {}".format(val)) - yield self._line('c.{} = {!r}\n'.format(opt.name, value)) + yield self._line('c.{} = {!r}'.format(opt.name, value)) + yield '' def _gen_bindings(self): """Generate the bindings part of the config.""" diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 6233369f7..cef035a1f 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -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 @@ -527,6 +529,138 @@ 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 + + @pytest.fixture def init_patch(qapp, fake_save_manager, config_tmpdir, data_tmpdir, config_stub, monkeypatch): From 3cb93b22ae78cd0f12de5e0f6bb1f5bf9b8ec667 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 10:34:25 +0200 Subject: [PATCH 08/13] Add tests for :config-write-py --- tests/unit/config/test_configcommands.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 9c6a9e460..359a4b23a 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -345,6 +345,55 @@ class TestEdit: assert msg.text == expected +class TestWritePy: + + """Tests for :config-write-py.""" + + def test_custom(self, commands, config_stub, keyconf, tmpdir): + confpy = tmpdir / 'config.py' + config_stub.val.content.javascript.enabled = True + keyconf.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.""" From 712e4a975af17bc878f9ea241814b155b9752fb4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 10:48:14 +0200 Subject: [PATCH 09/13] Add a test to make sure :write-config-py files actually work --- qutebrowser/config/configfiles.py | 3 +++ tests/unit/config/test_configfiles.py | 28 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index aab210bf1..7be756b2d 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -348,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) diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index cef035a1f..6339784e9 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -273,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") @@ -342,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') @@ -660,6 +657,21 @@ class TestConfigPyWriter: 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, From 012f79b244bf0da17b00f0dc3332aaa070cad4f0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 11:05:16 +0200 Subject: [PATCH 10/13] Make sure key_config_stub is available when needed --- tests/unit/config/test_configcommands.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 359a4b23a..96517d81c 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -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): From c63d16e2eaa3af45d49c7d44d1ff2711636d78b0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 11:09:07 +0200 Subject: [PATCH 11/13] tests: Get rid of duplicate key config stubs --- tests/unit/config/conftest.py | 29 ----------- tests/unit/config/test_config.py | 63 ++++++++++++------------ tests/unit/config/test_configcommands.py | 22 +++++---- 3 files changed, 43 insertions(+), 71 deletions(-) delete mode 100644 tests/unit/config/conftest.py diff --git a/tests/unit/config/conftest.py b/tests/unit/config/conftest.py deleted file mode 100644 index 89767b786..000000000 --- a/tests/unit/config/conftest.py +++ /dev/null @@ -1,29 +0,0 @@ -# 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 a46e8e3de..e42aca1a8 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -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'), ('', ''), ]) - 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', '', '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', '': '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: ('', '') ]) @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', '': 'nop'}, 'caret': {'a': 'nop', '': '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: diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 96517d81c..21ec040f7 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -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: @@ -353,10 +353,10 @@ class TestWritePy: """Tests for :config-write-py.""" - def test_custom(self, commands, config_stub, keyconf, tmpdir): + def test_custom(self, commands, config_stub, key_config_stub, tmpdir): confpy = tmpdir / 'config.py' config_stub.val.content.javascript.enabled = True - keyconf.bind(',x', 'message-info foo', mode='normal') + key_config_stub.bind(',x', 'message-info foo', mode='normal') commands.config_write_py(str(confpy)) @@ -408,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 @@ -466,7 +467,7 @@ class TestBind: commands.bind('a', 'nop', mode='wrongmode') @pytest.mark.parametrize('key', ['a', 'b', '']) - 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 @@ -479,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 @@ -495,7 +496,8 @@ class TestBind: ('c', 'c'), # :bind then :unbind ('', '') # 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', '': 'nop'}, 'caret': {'a': 'nop', '': 'nop'}, @@ -509,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': From 581b09b08fbf7d66480cd056a73c1b3e3a73224a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 11:10:53 +0200 Subject: [PATCH 12/13] Update changelog --- doc/changelog.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 91b8efb66..29bd527d3 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -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 From 953d028bd6056d516b4b7e612b92ea8c0b207622 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 5 Oct 2017 11:22:34 +0200 Subject: [PATCH 13/13] Mention config commands in configuring.asciidoc --- doc/help/configuring.asciidoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index e296eaac4..9c350cbaf 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -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