From 00c8d8da343071c6e518d3a41809d06bee5f5bee Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 8 Jun 2017 17:43:34 +0200 Subject: [PATCH 001/516] Initial stubbing out of a new config --- qutebrowser/app.py | 3 +- qutebrowser/commands/runners.py | 2 + qutebrowser/completion/models/miscmodels.py | 9 +-- qutebrowser/config/config.py | 14 ++++- qutebrowser/config/configdata.py | 63 ++++++++++++--------- qutebrowser/config/newconfig.py | 61 ++++++++++++++++++++ qutebrowser/config/style.py | 13 ++--- 7 files changed, 123 insertions(+), 42 deletions(-) create mode 100644 qutebrowser/config/newconfig.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index a3a855f9a..59aff9476 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -148,7 +148,8 @@ def init(args, crash_handler): log.init.debug("Connecting signals...") config_obj = objreg.get('config') - config_obj.style_changed.connect(style.get_stylesheet.cache_clear) + # FIXME:conf + # config_obj.style_changed.connect(style.get_stylesheet.cache_clear) qApp.focusChanged.connect(on_focus_changed) _process_args(args) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index cc967ce86..c9109ab78 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -106,6 +106,8 @@ class CommandRunner(QObject): The new command string if an alias was found. Default value otherwise. """ + # FIXME:conf + return default parts = text.strip().split(maxsplit=1) try: alias = config.get('aliases', parts[0]) diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 5ab381c43..00d1cdd02 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -285,9 +285,10 @@ def _get_cmd_completions(include_hidden, include_aliases, prefix=''): bindings = ', '.join(cmd_to_keys.get(obj.name, [])) cmdlist.append((prefix + obj.name, obj.desc, bindings)) - if include_aliases: - for name, cmd in config.section('aliases').items(): - bindings = ', '.join(cmd_to_keys.get(name, [])) - cmdlist.append((name, "Alias for '{}'".format(cmd), bindings)) + # FIXME:conf + # if include_aliases: + # for name, cmd in config.section('aliases').items(): + # bindings = ', '.join(cmd_to_keys.get(name, [])) + # cmdlist.append((name, "Alias for '{}'".format(cmd), bindings)) return cmdlist diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 587da214f..fe3beb545 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -37,7 +37,7 @@ import collections.abc from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings from PyQt5.QtGui import QColor -from qutebrowser.config import configdata, configexc, textwrapper +from qutebrowser.config import configdata, configexc, textwrapper, newconfig from qutebrowser.config.parsers import keyconf from qutebrowser.config.parsers import ini from qutebrowser.commands import cmdexc, cmdutils @@ -132,7 +132,8 @@ def get(*args, **kwargs): def section(sect): """Get a config section from the global config.""" - return objreg.get('config')[sect] + config = objreg.get('config') + return newconfig.SectionStub(config, sect) def _init_main_config(parent=None): @@ -242,13 +243,20 @@ def _init_misc(): QSettings.setPath(fmt, QSettings.UserScope, path) +def _init_new_config(parent): + new_config = newconfig.NewConfigManager(parent) + new_config.read_defaults() + objreg.register('config', new_config) + + def init(parent=None): """Initialize the config. Args: parent: The parent to pass to QObjects which get initialized. """ - _init_main_config(parent) + # _init_main_config(parent) + _init_new_config(parent) _init_key_config(parent) _init_misc() diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 28cf41ac5..711fca495 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -27,6 +27,8 @@ DATA: A global read-only copy of the default config, an OrderedDict of sections. """ +# FIXME:conf reintroduce interpolation? + import sys import re import collections @@ -121,6 +123,13 @@ SECTION_DESC = { DEFAULT_FONT_SIZE = '10pt' if sys.platform == 'darwin' else '8pt' +# FIXME:conf what to do about this? +MONOSPACE = (' xos4 Terminus, Terminus, Monospace, ' + '"DejaVu Sans Mono", Monaco, ' + '"Bitstream Vera Sans Mono", "Andale Mono", ' + '"Courier New", Courier, "Liberation Mono", ' + 'monospace, Fixed, Consolas, Terminal') + def data(readonly=False): @@ -155,7 +164,7 @@ def data(readonly=False): "the filename will be appended."), ('default-page', - SettingValue(typ.FuzzyUrl(), '${startpage}'), + SettingValue(typ.FuzzyUrl(), 'https://start.duckduckgo.com/'), "The page to open if :open -t/-b/-w is used without URL. Use " "`about:blank` for a blank page."), @@ -1051,7 +1060,7 @@ def data(readonly=False): "Top border color of the completion widget category headers."), ('completion.category.border.bottom', - SettingValue(typ.QssColor(), '${completion.category.border.top}'), + SettingValue(typ.QssColor(), 'black'), "Bottom border color of the completion widget category headers."), ('completion.item.selected.fg', @@ -1068,7 +1077,7 @@ def data(readonly=False): ('completion.item.selected.border.bottom', SettingValue( - typ.QssColor(), '${completion.item.selected.border.top}'), + typ.QssColor(), '#bbbb00'), "Bottom border color of the selected completion item."), ('completion.match.fg', @@ -1076,11 +1085,11 @@ def data(readonly=False): "Foreground color of the matched text in the completion."), ('completion.scrollbar.fg', - SettingValue(typ.QssColor(), '${completion.fg}'), + SettingValue(typ.QssColor(), 'white'), "Color of the scrollbar handle in completion view."), ('completion.scrollbar.bg', - SettingValue(typ.QssColor(), '${completion.bg}'), + SettingValue(typ.QssColor(), '#333333'), "Color of the scrollbar in completion view"), ('statusbar.fg', @@ -1092,7 +1101,7 @@ def data(readonly=False): "Background color of the statusbar."), ('statusbar.fg.private', - SettingValue(typ.QssColor(), '${statusbar.fg}'), + SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar in private browsing mode."), ('statusbar.bg.private', @@ -1100,7 +1109,7 @@ def data(readonly=False): "Background color of the statusbar in private browsing mode."), ('statusbar.fg.insert', - SettingValue(typ.QssColor(), '${statusbar.fg}'), + SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar in insert mode."), ('statusbar.bg.insert', @@ -1108,25 +1117,25 @@ def data(readonly=False): "Background color of the statusbar in insert mode."), ('statusbar.fg.command', - SettingValue(typ.QssColor(), '${statusbar.fg}'), + SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar in command mode."), ('statusbar.bg.command', - SettingValue(typ.QssColor(), '${statusbar.bg}'), + SettingValue(typ.QssColor(), 'black'), "Background color of the statusbar in command mode."), ('statusbar.fg.command.private', - SettingValue(typ.QssColor(), '${statusbar.fg.private}'), + SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar in private browsing + command " "mode."), ('statusbar.bg.command.private', - SettingValue(typ.QssColor(), '${statusbar.bg.private}'), + SettingValue(typ.QssColor(), 'grey'), "Background color of the statusbar in private browsing + command " "mode."), ('statusbar.fg.caret', - SettingValue(typ.QssColor(), '${statusbar.fg}'), + SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar in caret mode."), ('statusbar.bg.caret', @@ -1134,7 +1143,7 @@ def data(readonly=False): "Background color of the statusbar in caret mode."), ('statusbar.fg.caret-selection', - SettingValue(typ.QssColor(), '${statusbar.fg}'), + SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar in caret mode with a " "selection"), @@ -1148,7 +1157,7 @@ def data(readonly=False): "Background color of the progress bar."), ('statusbar.url.fg', - SettingValue(typ.QssColor(), '${statusbar.fg}'), + SettingValue(typ.QssColor(), 'white'), "Default foreground color of the URL in the statusbar."), ('statusbar.url.fg.success', @@ -1200,11 +1209,11 @@ def data(readonly=False): "Background color of selected odd tabs."), ('tabs.fg.selected.even', - SettingValue(typ.QtColor(), '${tabs.fg.selected.odd}'), + SettingValue(typ.QtColor(), 'white'), "Foreground color of selected even tabs."), ('tabs.bg.selected.even', - SettingValue(typ.QtColor(), '${tabs.bg.selected.odd}'), + SettingValue(typ.QtColor(), 'black'), "Background color of selected even tabs."), ('tabs.bg.bar', @@ -1255,7 +1264,7 @@ def data(readonly=False): "Color gradient start for download backgrounds."), ('downloads.fg.stop', - SettingValue(typ.QtColor(), '${downloads.fg.start}'), + SettingValue(typ.QtColor(), '#0000aa'), "Color gradient end for download text."), ('downloads.bg.stop', @@ -1356,23 +1365,23 @@ def data(readonly=False): "Default monospace fonts."), ('completion', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used in the completion widget."), ('completion.category', - SettingValue(typ.Font(), 'bold ${completion}'), + SettingValue(typ.Font(), 'bold ' + DEFAULT_FONT_SIZE + MONOSPACE), "Font used in the completion categories."), ('tabbar', - SettingValue(typ.QtFont(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.QtFont(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used in the tab bar."), ('statusbar', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used in the statusbar."), ('downloads', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used for the downloadbar."), ('hints', @@ -1380,7 +1389,7 @@ def data(readonly=False): "Font used for the hints."), ('debug-console', - SettingValue(typ.QtFont(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.QtFont(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used for the debugging console."), ('web-family-standard', @@ -1432,19 +1441,19 @@ def data(readonly=False): "The default font size for fixed-pitch text."), ('keyhint', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used in the keyhint widget."), ('messages.error', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used for error messages."), ('messages.warning', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used for warning messages."), ('messages.info', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), "Font used for info messages."), ('prompts', diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py new file mode 100644 index 000000000..f4c350d66 --- /dev/null +++ b/qutebrowser/config/newconfig.py @@ -0,0 +1,61 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-2017 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""New qutebrowser configuration code.""" + + +from PyQt5.QtCore import pyqtSignal, QObject + + +from qutebrowser.config import configdata + + +class SectionStub: + + # FIXME get rid of this once we get rid of sections + + def __init__(self, conf, name): + self._conf = conf + self._name = name + + def __getitem__(self, item): + return self._conf.get(self._name, item) + + +class NewConfigManager(QObject): + + # FIXME:conf QObject? + + changed = pyqtSignal(str, str) # FIXME:conf stub... where is this used? + + def __init__(self, parent=None): + super().__init__(parent) + self._values = {} + + def _key(self, sect, opt): + return sect + ' -> ' + opt + + def read_defaults(self): + for name, section in configdata.data().items(): + for key, value in section.items(): + self._values[self._key(name, key)] = value + + def get(self, section, option): + val = self._values[self._key(section, option)] + return val.typ.transform(val.value()) diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index 15215c398..cc66af2d8 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -40,7 +40,7 @@ def get_stylesheet(template_str): Return: The formatted template as string. """ - colordict = ColorDict(config.section('colors')) + colordict = ColorDict(config) template = jinja2.Template(template_str) return template.render(color=colordict, font=config.section('fonts'), config=objreg.get('config')) @@ -70,10 +70,13 @@ def _update_stylesheet(obj): obj.setStyleSheet(get_stylesheet(obj.STYLESHEET)) -class ColorDict(collections.UserDict): +class ColorDict: """A dict aimed at Qt stylesheet colors.""" + def __init__(self, config): + self._config = config + def __getitem__(self, key): """Override dict __getitem__. @@ -86,11 +89,7 @@ class ColorDict(collections.UserDict): else, return the plain value. """ - try: - val = self.data[key] - except KeyError: - log.config.exception("No color defined for {}!".format(key)) - return '' + val = self._config.get('colors', key) if isinstance(val, QColor): # This could happen when accidentally declaring something as # QtColor instead of Color in the config, and it'd go unnoticed as From 836395cdb154e028a487a7857f77911d1f803a38 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 8 Jun 2017 21:45:07 +0200 Subject: [PATCH 002/516] Add configdata.yml --- MANIFEST.in | 1 + qutebrowser/config/configdata.yml | 1732 +++++++++++++++++++++++++++++ 2 files changed, 1733 insertions(+) create mode 100644 qutebrowser/config/configdata.yml diff --git a/MANIFEST.in b/MANIFEST.in index 303c13db7..88c320868 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,6 +17,7 @@ include requirements.txt include tox.ini include qutebrowser.py include misc/cheatsheet.svg +include qutebrowser/config/configdata.yml prune www prune scripts/dev diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml new file mode 100644 index 000000000..f39b1912b --- /dev/null +++ b/qutebrowser/config/configdata.yml @@ -0,0 +1,1732 @@ +# general + +ignore_case: + type: IgnoreCase + default: smart + desc: Whether to find text on a page case-insensitively. + +startpage: + type: + name: List + elemtype: String + default: ["https://start.duckduckgo.com"] + desc: The default page(s) to open at the start. + +yank_ignored_url_parameters: + type: + name: List + elemtype: String + default: + - ref + - utm_source + - utm_medium + - utm_campaign + - utm_term + - utm_content + desc: The URL parameters to strip with :yank url. + +default_open_dispatcher: + type: + name: String + none_ok: true + default: "" + desc: >- + The default program used to open downloads. Set to an empty string to use + the default internal handler. + + Any `{}` in the string will be expanded to the filename, else the filename + will be appended. + +default_page: + type: FuzzyUrl + default: https://start.duckduckgo.com/ + desc: >- + The page to open if :open -t/-b/-w is used without URL. Use `about:blank` + for a blank page. + +auto_search: + type: AutoSearch + default: naive + desc: Whether to start a search when something else than a URL is entered. + +auto_save_config: + type: Bool + default: true + desc: Whether to save the config automatically on quit. + +auto_save_interval: + type: + name: Int + minval: 0 + maxval: maxint + default: 15000 + desc: How often (in milliseconds) to auto-save config/cookies/etc. + +editor: + type: + name: ShellCommand + placeholder: true + default: "gvim -f '{}'" + desc: >- + The editor (and arguments) to use for the `open-editor` command. + + The arguments get split like in a shell, so you can use `\"` or `\'` to quote + them. + + `{}` gets replaced by the filename of the file to be edited. + +editor_encoding: + type: Encoding + default: utf-8 + desc: Encoding to use for the editor. + +private_browsing: + type: Bool + default: false + desc: Open new windows in private browsing mode which does not record visited + pages. + +developer_extras: + type: Bool + default: false + backend: QtWebKit + desc: >- + Enable extra tools for Web developers. + + This needs to be enabled for `:inspector` to work and also adds an _Inspect_ + entry to the context menu. For QtWebEngine, see `qutebrowser --help` + instead. + +print_element_backgrounds: + type: Bool + default: true + backend: + QtWebKit: true + QtWebEngine: Qt 5.8 + desc: >- + Whether the background color and images are also drawn when the page is + printed. + + # FIXME:conf This should be added automatically: + # This setting only works with Qt 5.8 or newer when using the QtWebEngine backend. + +xss_auditing: + type: Bool + default: false + desc: >- + Whether load requests should be monitored for cross-site cripting attempts. + + Suspicious scripts will be blocked and reported in the inspector\'s + JavaScript console. Enabling this feature might have an impact on + performance. + +default_encoding: + type: String + default: iso-8859-1 + desc: >- + Default encoding to use for websites. + + The encoding must be a string describing an encoding such as _utf-8_, + _iso-8859-1_, etc. + +new_instance_open_target: + type: + name: String + valid_values: + - tab: Open a new tab in the existing window and activate the window. + - tab-bg: Open a new background tab in the existing window and activate + the window. + - tab-silent: Open a new tab in the existing window without activating the + window. + - tab-bg-silent: Open a new background tab in the existing window without + activating the window. + - window: Open in a new window. + default: tab + desc: How to open links in an existing instance if a new one is launched. + +new_instance_open_target_window: + type: + name: String + valid_values: + - first-opened: Open new tabs in the first (oldest) opened window. + - last-opened: Open new tabs in the last (newest) opened window. + - last-focused: Open new tabs in the most recently focused window. + - last-visible: Open new tabs in the most recently visible window. + default: last-focused + desc: Which window to choose when opening links as new tabs. + +log_javascript_console: + type: + name: String + valid_values: + - none: "Don't log messages." + - debug: Log messages with debug level. + - info: Log messages with info level. + default: debug + desc: How to log javascript console messages. + +save_session: + type: Bool + default: false + desc: Whether to always save the open pages. + +session_default_name: + type: + name: SessionName + none_ok: true + default: "" + desc: The name of the session to save by default, or empty for the last loaded + session. + +url_incdec_segments: + type: + name: FlagList + valid_values: [host, path, query, anchor] + default: [path, query] + desc: The URL segments where `:navigate increment/decrement` will search for a + number. + + +# ui + +history_session_interval: + type: Int + default: 30 + desc: The maximum time in minutes between two history items for them to be considered + being from the same session. Use -1 to disable separation. + +zoom_levels: + type: + name: List + elemtype: + name: Perc + minval: 0 + default: + - 25% + - 33% + - 50% + - 67% + - 75% + - 90% + - 100% + - 110% + - 125% + - 150% + - 175% + - 200% + - 250% + - 300% + - 400% + - 500% + desc: The available zoom levels. + +default_zoom: + type: Perc + default: 100% + desc: The default zoom level. + +downloads_position: + type: VerticalPosition + default: top + desc: Where to show the downloaded files. + +status_position: + type: VerticalPosition + default: bottom + desc: The position of the status bar. + +message_timeout: + type: + name: Int + minval: 0 + default: 2000 + desc: >- + Time (in ms) to show messages in the statusbar for. + + Set to 0 to never clear messages. + +message_unfocused: + type: Bool + default: false + desc: Whether to show messages in unfocused windows. + +confirm_quit: + type: ConfirmQuit + default: never + desc: Whether to confirm quitting the application. + +zoom_text_only: + type: Bool + default: false + backend: QtWebKit + desc: Whether the zoom factor on a frame applies only to the text or to all + content. + +frame_flattening: + default: false + type: Bool + backend: QtWebKit + desc: >- + Whether to expand each subframe to its contents. + + This will flatten all the frames to become one scrollable page. + +user_stylesheet: + type: + name: File + none_ok: True + default: "" + desc: User stylesheet to use (absolute filename or filename relative to the config + directory). Will expand environment variables. + +hide_scrollbar: + type: Bool + default: true + desc: Hide the main scrollbar. + +smooth_scrolling: + type: Bool + default: false + desc: Whether to enable smooth scrolling for web pages. Note smooth scrolling does + not work with the `:scroll-px` command. + +remove_finished_downloads: + default: -1 + type: + name: Int + minval: -1 + desc: Number of milliseconds to wait before removing finished downloads. Will not + be removed if value is -1. + +hide_statusbar: + type: Bool + default: false + desc: Whether to hide the statusbar unless a message is shown. + +statusbar_padding: + type: Padding + default: + top: 1 + bottom: 1 + left: 0 + right: 0 + desc: Padding for the statusbar. + +window_title_format: + type: + name: FormatString + fields: + - perc + - perc_raw + - title + - title_sep + - id + - scroll_pos + - host + - backend + - private + default: '{perc}{title}{title_sep}qutebrowser' + desc: | + The format to use for the window title. The following placeholders are defined: + + * `{perc}`: The percentage as a string like `[10%]`. + * `{perc_raw}`: The raw percentage, e.g. `10` + * `{title}`: The title of the current web page + * `{title_sep}`: The string ` - ` if a title is set, empty otherwise. + * `{id}`: The internal window ID of this window. + * `{scroll_pos}`: The page scroll position. + * `{host}`: The host of the current web page. + * `{backend}`: Either ''webkit'' or ''webengine'' + * `{private}` : Indicates when private mode is enabled. + + +modal_js_dialog: + type: Bool + default: false + desc: Use the standard JavaScript modal dialog for `alert()` and `confirm()` + +hide_wayland_decoration: + type: Bool + default: false + desc: Hide the window decoration when using wayland (requires restart) + +keyhint_blacklist: + type: + name: List + elemtype: + name: String + none_ok: true + default: "" + desc: >- + Keychains that shouldn\'t be shown in the keyhint dialog. + + Globs are supported, so `;*` will blacklist all keychains starting with `;`. + Use `*` to disable keyhints. + +keyhint_delay: + type: + name: Int + minval: 0 + default: 500 + desc: Time from pressing a key to seeing the keyhint dialog (ms) + +prompt_radius: + type: + name: Int + minval: 0 + default: 8 + desc: The rounding radius for the edges of prompts. + +prompt_filebrowser: + type: Bool + default: true + desc: Show a filebrowser in upload/download prompts. + + +# network + +do_not_track: + type: Bool + default: true + desc: Value to send in the `DNT` header. + +accept_language: + type: String + default: en-US,en + desc: Value to send in the `accept-language` header. + +referer_header: + default: same-domain + type: + name: String + valid_values: + - always: "Always send." + - never: "Never send; this is not recommended, as some sites may break." + - same-domain: "Only send for the same domain. This will still protect + your privacy, but shouldn't break any sites." + backend: QtWebKit + desc: Send the Referer header + +user_agent: + default: "" + type: + name: UserAgent + none_ok: true + desc: User agent to send. Empty to send the default. + +proxy: + default: system + type: + name: Proxy + valid_values: + - system: "Use the system wide proxy." + - none: "Don't use any proxy" + backend: + QtWebKit: true + QtWebEngine: Qt 5.8 + desc: >- + The proxy to use. + + In addition to the listed values, you can use a `socks://...` or `http://...` + URL. + + # FIXME:conf This should be added automatically: + # This setting only works with Qt 5.8 or newer when using the QtWebEngine + # backend. + +proxy_dns_requests: + default: true + type: Bool + backend: QtWebKit + desc: Whether to send DNS requests over the configured proxy. + +ssl_strict: + default: ask + type: BoolAsk + desc: Whether to validate SSL handshakes. + +dns_prefetch: + default: true + type: Bool + backend: QtWebKit + desc: Whether to try to pre-fetch DNS entries to speed up browsing. + +custom_headers: + default: {} + type: + name: Dict + keytype: String + valtype: String + none_ok: true + desc: Set custom headers for qutebrowser HTTP requests. + +netrc_file: + default: "" + type: + name: File + none_ok: true + desc: Set location of a netrc-file for HTTP authentication. If empty, ~/.netrc + is used. + + +# completion + +show: + default: always + type: + name: String + valid_values: + - always: Whenever a completion is available. + - auto: Whenever a completion is requested. + - never: Never. + desc: When to show the autocompletion window. + +download_path_suggestion: + default: path + type: + name: String + valid_values: + - path: Show only the download path. + - filename: Show only download filename. + - both: Show download path and filename. + desc: What to display in the download filename input. + +timestamp_format: + type: + name: TimestampTemplate + none_ok: true + default: '%Y-%m-%d' + desc: How to format timestamps (e.g. for history) + +height: + type: + name: PercOrInt + minperc: 0 + maxperc: 100 + minint: 1 + default: 50% + desc: The height of the completion, in px or as percentage of the window. + +cmd_history_max_items: + default: 100 + type: + name: Int + minval: -1 + desc: >- + How many commands to save in the command history. + + 0: no history / -1: unlimited + +web_history_max_items: + default: 1000 + type: + name: Int + minval: -1 + desc: >- + How many URLs to show in the web history. + + 0: no history / -1: unlimited + +quick_complete: + default: true + type: Bool + desc: "Whether to move on to the next part when there's only one possible completion + left." + +shrink: + default: false + type: Bool + desc: Whether to shrink the completion to be smaller than the configured size if + there are no scrollbars. + +scrollbar_width: + default: 12 + type: + name: Int + minval: 0 + desc: Width of the scrollbar in the completion window (in px). + +scrollbar_padding: + default: 2 + type: + name: Int + minval: 0 + desc: Padding of scrollbar handle in completion window (in px). + + +# input + +timeout: + default: 500 + type: + name: Int + minval: 0 + maxval: maxint + desc: >- + Timeout (in milliseconds) for ambiguous key bindings. + + If the current input forms both a complete match and a partial match, the complete + match will be executed after this time. + +partial_timeout: + default: 5000 + type: + name: Int + minval: 0 + maxval: maxint + desc: >- + Timeout (in milliseconds) for partially typed key bindings. + + If the current input forms only partial matches, the keystring will be cleared + after this time. + +insert_mode_on_plugins: + default: false + type: Bool + desc: Whether to switch to insert mode when clicking flash and other plugins. + +auto_leave_insert_mode: + default: true + type: Bool + desc: Whether to leave insert mode if a non-editable element is clicked. + +auto_insert_mode: + default: false + type: Bool + desc: Whether to automatically enter insert mode if an editable element is focused + after page load. + +forward_unbound_keys: + default: auto + type: + name: String + valid_values: + - all: "Forward all unbound keys." + - auto: "Forward unbound non-alphanumeric keys." + - none: "Don't forward any keys." + desc: Whether to forward unbound keys to the webview in normal mode. + +spatial_navigation: + default: false + type: Bool + desc: >- + Enables or disables the Spatial Navigation feature. + + Spatial navigation consists in the ability to navigate between focusable elements + in a Web page, such as hyperlinks and form controls, by using Left, Right, Up + and Down arrow keys. For example, if a user presses the Right key, heuristics + determine whether there is an element he might be trying to reach towards the + right and which element he probably wants. + +links_included_in_focus_chain: + default: true + type: Bool + desc: Whether hyperlinks should be included in the keyboard focus chain. + +rocker_gestures: + default: false + type: Bool + desc: Whether to enable Opera-like mouse rocker gestures. This disables the context + menu. + +mouse_zoom_divider: + default: 512 + type: + name: Int + minval: 0 + desc: How much to divide the mouse wheel movements to translate them into zoom increments. + + +# tabs + +background_tabs: + default: false + type: Bool + desc: Whether to open new tabs (middleclick/ctrl+click) in background. + +select_on_remove: + default: 'next' + type: SelectOnRemove + desc: Which tab to select when the focused tab is removed. + +new_tab_position: + default: next + type: NewTabPosition + desc: How new tabs are positioned. + +new_tab_position_explicit: + default: last + type: NewTabPosition + desc: How new tabs opened explicitly are positioned. + +last_close: + default: ignore + type: + name: String + valid_values: + - ignore: "Don't do anything." + - blank: "Load a blank page." + - startpage: "Load the start page." + - default-page: "Load the default page." + - close: "Close the window." + desc: Behavior when the last tab is closed. + +show: + default: always + type: + name: String + valid_values: + - always: Always show the tab bar. + - never: Always hide the tab bar. + - multiple: Hide the tab bar if only one tab is open. + - switching: Show the tab bar when switching tabs. + desc: When to show the tab bar. + +show_switching_delay: + default: 800 + type: Int + desc: "Time to show the tab bar before hiding it when tabs->show is set to + 'switching'." + +wrap: + default: true + type: Bool + desc: Whether to wrap when changing tabs. + +movable: + default: true + type: Bool + desc: Whether tabs should be movable. + +close_mouse_button: + default: middle + type: + name: String + valid_values: + - right: "Close tabs on right-click." + - middle: "Close tabs on middle-click." + - none: "Don't close tabs using the mouse." + desc: On which mouse button to close tabs. + +position: + default: top + type: Position + desc: The position of the tab bar. + +show_favicons: + default: true + desc: Whether to show favicons in the tab bar. + type: Bool + +favicon_scale: + default: 1.0 + type: + name: Float + minval: 0.0 + desc: Scale for favicons in the tab bar. The tab size is unchanged, so big favicons + also require extra `tabs->padding`. + +width: + default: 20% + type: + name: PercOrInt + minperc: 0 + maxperc: 100 + minint: 1 + desc: "The width of the tab bar if it's vertical, in px or as percentage of the window." + +pinned_width: + default: 43 + type: + name: Int + minval: 10 + desc: The width for pinned tabs with a horizontal tabbar, in px. + +indicator_width: + default: 3 + type: + name: Int + minval: 0 + desc: Width of the progress indicator (0 to disable). + +tabs_are_windows: + default: false + type: Bool + desc: Whether to open windows instead of tabs. + +title_format: + default: '{index}: {title}' + type: + name: FormatString + fields: + - perc + - perc_raw + - title + - title_sep + - index + - id + - scroll_pos + - host + - private + none_ok: true + desc: | + The format to use for the tab title. The following placeholders are defined: + + * `{perc}`: The percentage as a string like `[10%]`. + * `{perc_raw}`: The raw percentage, e.g. `10` + * `{title}`: The title of the current web page + * `{title_sep}`: The string ` - ` if a title is set, empty otherwise. + * `{index}`: The index of this tab. + * `{id}`: The internal tab ID of this tab. + * `{scroll_pos}`: The page scroll position. + * `{host}`: The host of the current web page. + * `{backend}`: Either ''webkit'' or ''webengine'' + * `{private}` : Indicates when private mode is enabled. + +title_format_pinned: + default: '{index}' + type: + name: FormatString + fields: + - perc + - perc_raw + - title + - title_sep + - index + - id + - scroll_pos + - host + - private + none_ok: true + desc: The format to use for the tab title for pinned tabs. The same placeholders + like for title-format are defined. + +title_alignment: + default: left + type: TextAlignment + desc: Alignment of the text inside of tabs + +mousewheel_tab_switching: + default: true + type: Bool + desc: Switch between tabs using the mouse wheel. + +padding: + default: + top: 0 + bottom: 0 + left: 5 + right: 5 + type: Padding + desc: Padding for tabs + +indicator_padding: + default: + top: 2 + bottom: 2 + left: 0 + right: 4 + type: Padding + desc: Padding for indicators + + +# storage + +download_directory: + default: "" + type: + name: Directory + none_ok: true + desc: The directory to save downloads to. An empty value selects a sensible os-specific + default. Will expand environment variables. + +prompt_download_directory: + default: true + type: Bool + desc: >- + Whether to prompt the user for the download location. + + If set to false, `download-directory` will be used. + +remember_download_directory: + default: true + type: Bool + desc: Whether to remember the last used download directory. + +# Defaults from QWebSettings::QWebSettings() in +# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp + +maximum_pages_in_cache: + default: 0 + type: + name: Int + minval: 0 + maxval: maxint + backend: QtWebKit + desc: >- + The maximum number of pages to hold in the global memory page cache. + + The Page Cache allows for a nicer user experience when navigating forth or back + to pages in the forward/back history, by pausing and resuming up to _n_ pages. + + For more information about the feature, please refer to: http://webkit.org/blog/427/webkit-page-cache-i-the-basics/ + +offline_web_application_cache: + default: true + type: Bool + backend: QtWebKit + desc: >- + Whether support for the HTML 5 web application cache feature is enabled. + + An application cache acts like an HTTP cache in some sense. For documents that + use the application cache via JavaScript, the loader engine will first ask the + application cache for the contents, before hitting the network. + + The feature is described in details at: http://dev.w3.org/html5/spec/Overview.html#appcache + +local_storage: + default: true + type: Bool + desc: Whether support for HTML 5 local storage and Web SQL is enabled. + +cache_size: + default: null + type: + name: Int + none_ok: true + minval: 0 + maxval: maxint64 + desc: Size of the HTTP network cache. Empty to use the default value. + + +# content + +allow_images: + default: true + type: Bool + desc: Whether images are automatically loaded in web pages. + +allow_javascript: + default: true + type: Bool + desc: Enables or disables the running of JavaScript programs. + +allow_plugins: + default: false + type: Bool + desc: Enables or disables plugins in Web pages. + +webgl: + default: true + type: Bool + desc: Enables or disables WebGL. + +hyperlink_auditing: + default: false + type: Bool + desc: Enable or disable hyperlink auditing (``). + +geolocation: + default: ask + type: BoolAsk + desc: Allow websites to request geolocations. + +notifications: + default: ask + type: BoolAsk + desc: Allow websites to show notifications. + +media_capture: + default: ask + type: BoolAsk + backend: QtWebEngine + desc: Allow websites to record audio/video. + +javascript_can_open_windows_automatically: + default: false + type: Bool + desc: Whether JavaScript programs can open new windows without user + interaction. + +javascript_can_close_windows: + default: false + type: Bool + backend: QtWebKit + desc: Whether JavaScript programs can close windows. + +javascript_can_access_clipboard: + default: false + type: Bool + desc: >- + Whether JavaScript programs can read or write to the clipboard. + + With QtWebEngine, writing the clipboard as response to a user interaction is always + allowed. + +ignore_javascript_prompt: + default: false + type: Bool + desc: Whether all javascript prompts should be ignored. + +ignore_javascript_alert: + default: false + type: Bool + desc: Whether all javascript alerts should be ignored. + +local_content_can_access_remote_urls: + default: false + type: Bool + desc: Whether locally loaded documents are allowed to access remote urls. + +local_content_can_access_file_urls: + default: true + type: Bool + desc: Whether locally loaded documents are allowed to access other local urls. + +cookies_accept: + default: no-3rdparty + backend: QtWebKit + type: + name: String + valid_values: + - all: "Accept all cookies." + - no-3rdparty: "Accept cookies from the same origin only." + - no-unknown-3rdparty: "Accept cookies from the same origin only, unless a + cookie is already set for the domain." + - never: "Don't accept cookies at all." + desc: Control which cookies to accept. + +cookies_store: + default: true + type: Bool + desc: Whether to store cookies. Note this option needs a restart with QtWebEngine + on Qt < 5.9. + +host_block_lists: + default: + - "https://www.malwaredomainlist.com/hostslist/hosts.txt" + - "http://someonewhocares.org/hosts/hosts" + - "http://winhelp2002.mvps.org/hosts.zip" + - "http://malwaredomains.lehigh.edu/files/justdomains.zip" + - "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext" + type: + name: List + elemtype: Url + none_ok: true + desc: | + List of URLs of lists which contain hosts to block. + + The file can be in one of the following formats: + + - An `/etc/hosts`-like file + - One host per line + - A zip-file of any of the above, with either only one file, or a file named + `hosts` (with any extension). + +host_blocking_enabled: + default: true + type: Bool + desc: Whether host blocking is enabled. + +host_blocking_whitelist: + default: + - piwik.org + type: + name: List + valtype: string + none_ok: true + desc: >- + List of domains that should always be loaded, despite being ad-blocked. + + Domains may contain * and ? wildcards and are otherwise required to exactly match + the requested domain. + + Local domains are always exempt from hostblocking. + +enable_pdfjs: + default: false + type: Bool + desc: >- + Enable pdf.js to view PDF files in the browser. + + Note that the files can still be downloaded by clicking the download button in + the pdf.js viewer. + + +# hints + +border: + default: '1px solid #E3BE23' + type: String + desc: CSS border value for hints. + +mode: + default: letter + type: + name: String + valid_values: + - number: Use numeric hints. (In this mode you can also type letters from + the hinted element to filter and reduce the number of elements that + are hinted.) + - letter: Use the chars in the hints -> chars setting. + - word: Use hints words based on the html elements and the extra words. + desc: Mode to use for hints. + +chars: + default: asdfghjkl + type: + name: UniqueCharString + minlen: 2 + completions: + - ['asdfghjkl', "Home row"] + - ['aoeuidnths', "Home row (Dvorak)"] + - ['abcdefghijklmnopqrstuvwxyz', "All letters"] + desc: Chars used for hint strings. + +min_chars: + default: 1 + type: + name: Int + minval: 1 + desc: Minimum number of chars used for hint strings. + +scatter: + default: true + type: Bool + desc: Whether to scatter hint key chains (like Vimium) or not (like dwb). Ignored + for number hints. + +uppercase: + default: false + type: Bool + desc: Make chars in hint strings uppercase. + +dictionary: + default: /usr/share/dict/words + type: + name: File + required: false + desc: The dictionary file to be used by the word hints. + +auto_follow: + default: unique-match + type: + name: String + valid_values: + - always: "Auto-follow whenever there is only a single hint on a page." + - unique-match: "Auto-follow whenever there is a unique non-empty match in + either the hint string (word mode) or filter (number mode)." + - full-match: "Follow the hint when the user typed the whole hint (letter, + word or number mode) or the element's text (only in number mode)." + - never: "The user will always need to press Enter to follow a hint." + desc: Controls when a hint can be automatically followed without the user + pressing Enter. + +auto_follow_timeout: + default: 0 + type: Int + desc: A timeout (in milliseconds) to inhibit normal-mode key bindings after a + successful auto-follow. + +next_regexes: + default: + - "\\bnext\\b" + - "\\bmore\\b" + - "\\bnewer\\b" + - "\\b[>\u2192\u226B]\\b" + - "\\b(>>|\xBB)\\b" + - "\\bcontinue\\b" + type: + name: List + valtype: + name: Regex + flags: IGNORECASE + desc: "A comma-separated list of regexes to use for 'next' links." + +prev_regexes: + default: + - "\\bprev(ious)?\\b" + - "\\bback\\b" + - "\\bolder\\b" + - "\\b[<\u2190\u226A]\\b" + - "\\b(<<|\xAB)\\b" + type: + name: List + valtype: + name: Regex + flags: IGNORECASE + desc: A comma-separated list of regexes to use for 'prev' links. + +find_implementation: + default: python + type: + name: String + valid_values: + - javascript: Better but slower + - python: Slightly worse but faster + desc: Which implementation to use to find elements to hint. + +hide_unmatched_rapid_hints: + default: true + type: Bool + desc: Controls hiding unmatched hints in rapid mode. + +# searchengines + +searchengines: + default: + DEFAULT: https://duckduckgo.com/?q={} + type: + name: Dict + keytype: SearchEngineName + valtype: SearchEngineUrl + desc: FIXME + +# aliases + +aliases: + default: {} + type: + name: Dict + keytype: + name: String + forbidden: ' ' + valtype: Command + desc: Command aliases FIXME + +# colors + +colors.completion.fg: + default: white + type: QtColor + desc: Text color of the completion widget. + +colors.completion.bg: + default: '#333333' + type: QssColor + desc: Background color of the completion widget. + +colors.completion.alternate_bg: + default: '#444444' + type: QssColor + desc: Alternating background color of the completion widget. + +colors.completion.category.fg: + default: white + type: QtColor + desc: Foreground color of completion widget category headers. + +colors.completion.category.bg: + default: 'qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050)' + type: QssColor + desc: Background color of the completion widget category headers. + +colors.completion.category.border.top: + default: black + type: QssColor + desc: Top border color of the completion widget category headers. + +colors.completion.category.border.bottom: + default: black + type: QssColor + desc: Bottom border color of the completion widget category headers. + +colors.completion.item.selected.fg: + default: black + type: QtColor + desc: Foreground color of the selected completion item. + +colors.completion.item.selected.bg: + default: '#e8c000' + type: QssColor + desc: Background color of the selected completion item. + +colors.completion.item.selected.border.top: + default: '#bbbb00' + type: QssColor + desc: Top border color of the completion widget category headers. + +colors.completion.item.selected.border.bottom: + default: '#bbbb00' + type: QssColor + desc: Bottom border color of the selected completion item. + +colors.completion.match.fg: + default: '#ff4444' + type: QssColor + desc: Foreground color of the matched text in the completion. + +colors.completion.scrollbar.fg: + default: white + type: QssColor + desc: Color of the scrollbar handle in completion view. + +colors.completion.scrollbar.bg: + default: '#333333' + type: QssColor + desc: Color of the scrollbar in completion view + +colors.statusbar.fg: + default: white + type: QssColor + desc: Foreground color of the statusbar. + +colors.statusbar.bg: + default: black + type: QssColor + desc: Background color of the statusbar. + +colors.statusbar.fg.private: + default: white + type: QssColor + desc: Foreground color of the statusbar in private browsing mode. + +colors.statusbar.bg.private: + default: '#666666' + type: QssColor + desc: Background color of the statusbar in private browsing mode. + +colors.statusbar.fg.insert: + default: white + type: QssColor + desc: Foreground color of the statusbar in insert mode. + +colors.statusbar.bg.insert: + default: darkgreen + type: QssColor + desc: Background color of the statusbar in insert mode. + +colors.statusbar.fg.command: + default: white + type: QssColor + desc: Foreground color of the statusbar in command mode. + +colors.statusbar.bg.command: + default: black + type: QssColor + desc: Background color of the statusbar in command mode. + +colors.statusbar.fg.command.private: + default: white + type: QssColor + desc: Foreground color of the statusbar in private browsing + command mode. + +colors.statusbar.bg.command.private: + default: grey + type: QssColor + desc: Background color of the statusbar in private browsing + command mode. + +colors.statusbar.fg.caret: + default: white + type: QssColor + desc: Foreground color of the statusbar in caret mode. + +colors.statusbar.bg.caret: + default: purple + type: QssColor + desc: Background color of the statusbar in caret mode. + +colors.statusbar.fg.caret_selection: + default: white + type: QssColor + desc: Foreground color of the statusbar in caret mode with a selection + +colors.statusbar.bg.caret_selection: + default: '#a12dff' + type: QssColor + desc: Background color of the statusbar in caret mode with a selection + +colors.statusbar.progress.bg: + default: white + type: QssColor + desc: Background color of the progress bar. + +colors.statusbar.url.fg: + default: white + type: QssColor + desc: Default foreground color of the URL in the statusbar. + +colors.statusbar.url.fg.success: + default: white + type: QssColor + desc: Foreground color of the URL in the statusbar on successful load (http). + +colors.statusbar.url.fg.success.https: + default: lime + type: QssColor + desc: Foreground color of the URL in the statusbar on successful load (https). + +colors.statusbar.url.fg.error: + default: orange + type: QssColor + desc: Foreground color of the URL in the statusbar on error. + +colors.statusbar.url.fg.warn: + default: yellow + type: QssColor + desc: "Foreground color of the URL in the statusbar when there's a warning." + +colors.statusbar.url.fg.hover: + default: aqua + desc: Foreground color of the URL in the statusbar for hovered links. + type: QssColor + +colors.tabs.fg.odd: + default: white + type: QtColor + desc: Foreground color of unselected odd tabs. + +colors.tabs.bg.odd: + default: grey + type: QtColor + desc: Background color of unselected odd tabs. + +colors.tabs.fg.even: + default: white + type: QtColor + desc: Foreground color of unselected even tabs. + +colors.tabs.bg.even: + default: darkgrey + type: QtColor + desc: Background color of unselected even tabs. + +colors.tabs.fg.selected.odd: + default: white + type: QtColor + desc: Foreground color of selected odd tabs. + +colors.tabs.bg.selected.odd: + default: black + type: QtColor + desc: Background color of selected odd tabs. + +colors.tabs.fg.selected.even: + default: white + type: QtColor + desc: Foreground color of selected even tabs. + +colors.tabs.bg.selected.even: + default: black + type: QtColor + desc: Background color of selected even tabs. + +colors.tabs.bg.bar: + default: '#555555' + type: QtColor + desc: Background color of the tab bar. + +colors.tabs.indicator.start: + default: '#0000aa' + type: QtColor + desc: Color gradient start for the tab indicator. + +colors.tabs.indicator.stop: + default: '#00aa00' + type: QtColor + desc: Color gradient end for the tab indicator. + +colors.tabs.indicator.error: + default: '#ff0000' + type: QtColor + desc: Color for the tab indicator on errors.. + +colors.tabs.indicator.system: + default: rgb + type: ColorSystem + desc: Color gradient interpolation system for the tab indicator. + +colors.hints.fg: + default: black + type: QssColor + desc: Font color for hints. + +colors.hints.bg: + default: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), + stop:1 rgba(255, 197, 66, 0.8)) + type: QssColor + desc: Background color for hints. Note that you can use a `rgba(...)` value for + transparency. + +colors.hints.fg.match: + default: green + type: QssColor + desc: Font color for the matched part of hints. + +colors.downloads.bg.bar: + default: black + type: QssColor + desc: Background color for the download bar. + +colors.downloads.fg.start: + default: white + type: QtColor + desc: Color gradient start for download text. + +colors.downloads.bg.start: + default: '#0000aa' + type: QtColor + desc: Color gradient start for download backgrounds. + +colors.downloads.fg.stop: + default: '#0000aa' + type: QtColor + desc: Color gradient end for download text. + +colors.downloads.bg.stop: + default: '#00aa00' + type: QtColor + desc: Color gradient stop for download backgrounds. + +colors.downloads.fg.system: + default: rgb + type: ColorSystem + desc: Color gradient interpolation system for download text. + +colors.downloads.bg.system: + default: rgb + type: ColorSystem + desc: Color gradient interpolation system for download backgrounds. + +colors.downloads.fg.error: + default: white + type: QtColor + desc: Foreground color for downloads with errors. + +colors.downloads.bg.error: + default: red + type: QtColor + desc: Background color for downloads with errors. + +colors.webpage.bg: + default: white + type: + name: QtColor + none_ok: true + desc: "Background color for webpages if unset (or empty to use the theme's color)" + +colors.keyhint.fg: + default: '#FFFFFF' + type: QssColor + desc: Text color for the keyhint widget. + +colors.keyhint.fg.suffix: + default: '#FFFF00' + type: CssColor + desc: Highlight color for keys to complete the current keychain + +colors.keyhint.bg: + default: rgba(0, 0, 0, 80%) + type: QssColor + desc: Background color of the keyhint widget. + +colors.messages.fg.error: + default: white + type: QssColor + desc: Foreground color of an error message. + +colors.messages.bg.error: + default: red + type: QssColor + desc: Background color of an error message. + +colors.messages.border.error: + default: '#bb0000' + type: QssColor + desc: Border color of an error message. + +colors.messages.fg.warning: + default: white + type: QssColor + desc: Foreground color a warning message. + +colors.messages.bg.warning: + default: darkorange + type: QssColor + desc: Background color of a warning message. + +colors.messages.border.warning: + default: '#d47300' + type: QssColor + desc: Border color of an error message. + +colors.messages.fg.info: + default: white + type: QssColor + desc: Foreground color an info message. + +colors.messages.bg.info: + default: black + type: QssColor + desc: Background color of an info message. + +colors.messages.border.info: + default: '#333333' + type: QssColor + desc: Border color of an info message. + +colors.prompts.fg: + default: white + type: QssColor + desc: Foreground color for prompts. + +colors.prompts.bg: + default: darkblue + type: QssColor + desc: Background color for prompts. + +colors.prompts.selected.bg: + default: '#308cc6' + type: QssColor + desc: Background color for the selected item in filename prompts. + + +# fonts + +fonts.monospace: + default: xos4 Terminus, Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream + Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, + Fixed, Consolas, Terminal + type: Font + desc: Default monospace fonts. + +fonts.completion: + default: 8pt monospace + type: Font + desc: Font used in the completion widget. + +fonts.completion.category: + default: bold 8pt monospace + type: Font + desc: Font used in the completion categories. + +fonts.tabbar: + default: 8pt monospace + type: QtFont + desc: Font used in the tab bar. + +fonts.statusbar: + default: 8pt monospace + type: Font + desc: Font used in the statusbar. + +fonts.downloads: + default: 8pt monospace + type: Font + desc: Font used for the downloadbar. + +fonts.hints: + default: bold 13px monospace + type: Font + desc: Font used for the hints. + +fonts.debug_console: + default: 8pt monospace + type: QtFont + desc: Font used for the debugging console. + +fonts.web_family_standard: + default: '' + type: + name: FontFamily + none_ok: true + desc: Font family for standard fonts. + +fonts.web_family_fixed: + default: '' + type: + name: FontFamily + none_ok: true + desc: Font family for fixed fonts. + +fonts.web_family_serif: + default: '' + type: + name: FontFamily + none_ok: true + desc: Font family for serif fonts. + +fonts.web_family_sans_serif: + default: '' + type: + name: FontFamily + none_ok: true + desc: Font family for sans-serif fonts. + +fonts.web_family_cursive: + default: '' + type: + name: FontFamily + none_ok: true + desc: Font family for cursive fonts. + +fonts.web_family_fantasy: + default: '' + type: + name: FontFamily + none_ok: true + desc: Font family for fantasy fonts. + +# Defaults for web_size_* from WebEngineSettings::initDefaults in +# qtwebengine/src/core/web_engine_settings.cpp and +# QWebSettings::QWebSettings() in +# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp + +fonts.web_size_minimum: + default: 0 + type: + name: Int + minval: 0 + maxval: maxint + desc: The hard minimum font size. + +fonts.web_size_minimum_logical: + # This is 0 as default on QtWebKit, and 6 on QtWebEngine - so let's + # just go for 6 here. + default: 6 + type: + name: Int + minval: 0 + maxval: maxint + desc: The minimum logical font size that is applied when zooming out. + +fonts.web_size_default: + default: 16 + type: + name: Int + minval: 1 + maxval: maxint + desc: The default font size for regular text. + +fonts.web_size_default_fixed: + default: 13 + type: + name: Int + minval: 1 + maxval: maxint + desc: The default font size for fixed-pitch text. + +fonts.keyhint: + default: 8pt monospace + type: Font + desc: Font used in the keyhint widget. + +fonts.messages.error: + default: 8pt monospace + type: Font + desc: Font used for error messages. + +fonts.messages.warning: + default: 8pt monospace + type: Font + desc: Font used for warning messages. + +fonts.messages.info: + default: 8pt monospace + type: Font + desc: Font used for info messages. + +fonts.prompts: + default: 8pt sans-serif + type: Font + desc: Font used for prompts. From f96580509977d694ae3a5236bf661887af668009 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 9 Jun 2017 13:05:24 +0200 Subject: [PATCH 003/516] First setting renames --- qutebrowser/config/configdata.yml | 314 +++++++++++++++--------------- 1 file changed, 159 insertions(+), 155 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index f39b1912b..5245095f6 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -5,7 +5,7 @@ ignore_case: default: smart desc: Whether to find text on a page case-insensitively. -startpage: +start_page: type: name: List elemtype: String @@ -25,7 +25,7 @@ yank_ignored_url_parameters: - utm_content desc: The URL parameters to strip with :yank url. -default_open_dispatcher: +downloads.open_dispatcher: type: name: String none_ok: true @@ -49,12 +49,12 @@ auto_search: default: naive desc: Whether to start a search when something else than a URL is entered. -auto_save_config: +auto_save.config: type: Bool default: true desc: Whether to save the config automatically on quit. -auto_save_interval: +auto_save.interval: type: name: Int minval: 0 @@ -62,7 +62,7 @@ auto_save_interval: default: 15000 desc: How often (in milliseconds) to auto-save config/cookies/etc. -editor: +editor.command: type: name: ShellCommand placeholder: true @@ -75,18 +75,18 @@ editor: `{}` gets replaced by the filename of the file to be edited. -editor_encoding: +editor.encoding: type: Encoding default: utf-8 desc: Encoding to use for the editor. -private_browsing: +content.private_browsing: type: Bool default: false desc: Open new windows in private browsing mode which does not record visited pages. -developer_extras: +content.developer_extras: type: Bool default: false backend: QtWebKit @@ -97,7 +97,7 @@ developer_extras: entry to the context menu. For QtWebEngine, see `qutebrowser --help` instead. -print_element_backgrounds: +content.print_element_backgrounds: type: Bool default: true backend: @@ -110,17 +110,17 @@ print_element_backgrounds: # FIXME:conf This should be added automatically: # This setting only works with Qt 5.8 or newer when using the QtWebEngine backend. -xss_auditing: +content.xss_auditing: type: Bool default: false desc: >- - Whether load requests should be monitored for cross-site cripting attempts. + Whether load requests should be monitored for cross-site scripting attempts. Suspicious scripts will be blocked and reported in the inspector\'s JavaScript console. Enabling this feature might have an impact on performance. -default_encoding: +content.default_encoding: type: String default: iso-8859-1 desc: >- @@ -155,7 +155,7 @@ new_instance_open_target_window: default: last-focused desc: Which window to choose when opening links as new tabs. -log_javascript_console: +content.javascript.log: type: name: String valid_values: @@ -165,7 +165,7 @@ log_javascript_console: default: debug desc: How to log javascript console messages. -save_session: +auto_save.session: type: Bool default: false desc: Whether to always save the open pages. @@ -195,7 +195,7 @@ history_session_interval: desc: The maximum time in minutes between two history items for them to be considered being from the same session. Use -1 to disable separation. -zoom_levels: +zoom.levels: type: name: List elemtype: @@ -220,22 +220,22 @@ zoom_levels: - 500% desc: The available zoom levels. -default_zoom: +zoom.default: type: Perc default: 100% desc: The default zoom level. -downloads_position: +downloads.position: type: VerticalPosition default: top desc: Where to show the downloaded files. -status_position: +statusbar.position: type: VerticalPosition default: bottom desc: The position of the status bar. -message_timeout: +messages.timeout: type: name: Int minval: 0 @@ -245,7 +245,7 @@ message_timeout: Set to 0 to never clear messages. -message_unfocused: +messages.unfocused: type: Bool default: false desc: Whether to show messages in unfocused windows. @@ -255,14 +255,14 @@ confirm_quit: default: never desc: Whether to confirm quitting the application. -zoom_text_only: +zoom.text_only: type: Bool default: false backend: QtWebKit desc: Whether the zoom factor on a frame applies only to the text or to all content. -frame_flattening: +content.frame_flattening: default: false type: Bool backend: QtWebKit @@ -271,7 +271,7 @@ frame_flattening: This will flatten all the frames to become one scrollable page. -user_stylesheet: +content.user_stylesheet: type: name: File none_ok: True @@ -279,18 +279,19 @@ user_stylesheet: desc: User stylesheet to use (absolute filename or filename relative to the config directory). Will expand environment variables. -hide_scrollbar: +scrolling.bar: + # FIXME:conf meaning changed! type: Bool - default: true - desc: Hide the main scrollbar. + default: false + desc: Show a scrollbar. -smooth_scrolling: +scrolling.smooth: type: Bool default: false desc: Whether to enable smooth scrolling for web pages. Note smooth scrolling does not work with the `:scroll-px` command. -remove_finished_downloads: +downloads.remove_finished: default: -1 type: name: Int @@ -298,12 +299,12 @@ remove_finished_downloads: desc: Number of milliseconds to wait before removing finished downloads. Will not be removed if value is -1. -hide_statusbar: +statusbar.hide: type: Bool default: false desc: Whether to hide the statusbar unless a message is shown. -statusbar_padding: +statusbar.padding: type: Padding default: top: 1 @@ -312,7 +313,7 @@ statusbar_padding: right: 0 desc: Padding for the statusbar. -window_title_format: +window.title_format: type: name: FormatString fields: @@ -340,17 +341,17 @@ window_title_format: * `{private}` : Indicates when private mode is enabled. -modal_js_dialog: +content.javascript.modal_dialog: type: Bool default: false desc: Use the standard JavaScript modal dialog for `alert()` and `confirm()` -hide_wayland_decoration: +window.hide_wayland_decoration: type: Bool default: false desc: Hide the window decoration when using wayland (requires restart) -keyhint_blacklist: +keyhint.blacklist: type: name: List elemtype: @@ -363,21 +364,21 @@ keyhint_blacklist: Globs are supported, so `;*` will blacklist all keychains starting with `;`. Use `*` to disable keyhints. -keyhint_delay: +keyhint.delay: type: name: Int minval: 0 default: 500 desc: Time from pressing a key to seeing the keyhint dialog (ms) -prompt_radius: +prompt.radius: type: name: Int minval: 0 default: 8 desc: The rounding radius for the edges of prompts. -prompt_filebrowser: +prompt.filebrowser: type: Bool default: true desc: Show a filebrowser in upload/download prompts. @@ -385,17 +386,17 @@ prompt_filebrowser: # network -do_not_track: +content.do_not_track: type: Bool default: true desc: Value to send in the `DNT` header. -accept_language: +content.accept_language: type: String default: en-US,en desc: Value to send in the `accept-language` header. -referer_header: +content.referer_header: default: same-domain type: name: String @@ -407,14 +408,14 @@ referer_header: backend: QtWebKit desc: Send the Referer header -user_agent: +content.user_agent: default: "" type: name: UserAgent none_ok: true desc: User agent to send. Empty to send the default. -proxy: +content.proxy: default: system type: name: Proxy @@ -434,24 +435,24 @@ proxy: # This setting only works with Qt 5.8 or newer when using the QtWebEngine # backend. -proxy_dns_requests: +content.proxy_dns_requests: default: true type: Bool backend: QtWebKit desc: Whether to send DNS requests over the configured proxy. -ssl_strict: +content.ssl_strict: default: ask type: BoolAsk desc: Whether to validate SSL handshakes. -dns_prefetch: +content.dns_prefetch: default: true type: Bool backend: QtWebKit desc: Whether to try to pre-fetch DNS entries to speed up browsing. -custom_headers: +content.custom_headers: default: {} type: name: Dict @@ -460,7 +461,7 @@ custom_headers: none_ok: true desc: Set custom headers for qutebrowser HTTP requests. -netrc_file: +content.netrc_file: default: "" type: name: File @@ -471,7 +472,7 @@ netrc_file: # completion -show: +completion.show: default: always type: name: String @@ -481,7 +482,7 @@ show: - never: Never. desc: When to show the autocompletion window. -download_path_suggestion: +downloads.path_suggestion: default: path type: name: String @@ -491,14 +492,14 @@ download_path_suggestion: - both: Show download path and filename. desc: What to display in the download filename input. -timestamp_format: +completion.timestamp_format: type: name: TimestampTemplate none_ok: true default: '%Y-%m-%d' desc: How to format timestamps (e.g. for history) -height: +completion.height: type: name: PercOrInt minperc: 0 @@ -507,7 +508,7 @@ height: default: 50% desc: The height of the completion, in px or as percentage of the window. -cmd_history_max_items: +completion.cmd_history_max_items: default: 100 type: name: Int @@ -517,7 +518,7 @@ cmd_history_max_items: 0: no history / -1: unlimited -web_history_max_items: +completion.web_history_max_items: default: 1000 type: name: Int @@ -527,26 +528,26 @@ web_history_max_items: 0: no history / -1: unlimited -quick_complete: +completion.quick: default: true type: Bool desc: "Whether to move on to the next part when there's only one possible completion left." -shrink: +completion.shrink: default: false type: Bool desc: Whether to shrink the completion to be smaller than the configured size if there are no scrollbars. -scrollbar_width: +completion.scrollbar.width: default: 12 type: name: Int minval: 0 desc: Width of the scrollbar in the completion window (in px). -scrollbar_padding: +completion.scrollbar.padding: default: 2 type: name: Int @@ -556,7 +557,8 @@ scrollbar_padding: # input -timeout: +# FIXME:conf get rid of this? +input.ambiguous_timeout: default: 500 type: name: Int @@ -568,7 +570,7 @@ timeout: If the current input forms both a complete match and a partial match, the complete match will be executed after this time. -partial_timeout: +input.partial_timeout: default: 5000 type: name: Int @@ -580,23 +582,23 @@ partial_timeout: If the current input forms only partial matches, the keystring will be cleared after this time. -insert_mode_on_plugins: +input.insert_mode.plugins: default: false type: Bool desc: Whether to switch to insert mode when clicking flash and other plugins. -auto_leave_insert_mode: +input.insert_mode.auto_leave: default: true type: Bool desc: Whether to leave insert mode if a non-editable element is clicked. -auto_insert_mode: +input.insert_mode.auto_focused: default: false type: Bool desc: Whether to automatically enter insert mode if an editable element is focused after page load. -forward_unbound_keys: +input.forward_unbound_keys: default: auto type: name: String @@ -606,7 +608,7 @@ forward_unbound_keys: - none: "Don't forward any keys." desc: Whether to forward unbound keys to the webview in normal mode. -spatial_navigation: +input.spatial_navigation: default: false type: Bool desc: >- @@ -618,18 +620,18 @@ spatial_navigation: determine whether there is an element he might be trying to reach towards the right and which element he probably wants. -links_included_in_focus_chain: +input.links_included_in_focus_chain: default: true type: Bool desc: Whether hyperlinks should be included in the keyboard focus chain. -rocker_gestures: +input.rocker_gestures: default: false type: Bool desc: Whether to enable Opera-like mouse rocker gestures. This disables the context menu. -mouse_zoom_divider: +zoom.mouse_divider: default: 512 type: name: Int @@ -639,27 +641,27 @@ mouse_zoom_divider: # tabs -background_tabs: +tabs.background: default: false type: Bool desc: Whether to open new tabs (middleclick/ctrl+click) in background. -select_on_remove: +tabs.select_on_remove: default: 'next' type: SelectOnRemove desc: Which tab to select when the focused tab is removed. -new_tab_position: +tabs.new_position: default: next type: NewTabPosition desc: How new tabs are positioned. -new_tab_position_explicit: +tabs.new_position_explicit: default: last type: NewTabPosition desc: How new tabs opened explicitly are positioned. -last_close: +tabs.last_close: default: ignore type: name: String @@ -671,7 +673,7 @@ last_close: - close: "Close the window." desc: Behavior when the last tab is closed. -show: +tabs.show: default: always type: name: String @@ -682,23 +684,23 @@ show: - switching: Show the tab bar when switching tabs. desc: When to show the tab bar. -show_switching_delay: +tabs.show_switching_delay: default: 800 type: Int desc: "Time to show the tab bar before hiding it when tabs->show is set to 'switching'." -wrap: +tabs.wrap: default: true type: Bool desc: Whether to wrap when changing tabs. -movable: +tabs.movable: default: true type: Bool desc: Whether tabs should be movable. -close_mouse_button: +tabs.close_mouse_button: default: middle type: name: String @@ -708,17 +710,17 @@ close_mouse_button: - none: "Don't close tabs using the mouse." desc: On which mouse button to close tabs. -position: +tabs.position: default: top type: Position desc: The position of the tab bar. -show_favicons: +tabs.favicons.show: default: true desc: Whether to show favicons in the tab bar. type: Bool -favicon_scale: +tabs.favicons.scale: default: 1.0 type: name: Float @@ -726,7 +728,7 @@ favicon_scale: desc: Scale for favicons in the tab bar. The tab size is unchanged, so big favicons also require extra `tabs->padding`. -width: +tabs.width.bar: default: 20% type: name: PercOrInt @@ -735,26 +737,26 @@ width: minint: 1 desc: "The width of the tab bar if it's vertical, in px or as percentage of the window." -pinned_width: +tabs.width.pinned: default: 43 type: name: Int minval: 10 desc: The width for pinned tabs with a horizontal tabbar, in px. -indicator_width: +tabs.width.indicator: default: 3 type: name: Int minval: 0 desc: Width of the progress indicator (0 to disable). -tabs_are_windows: +tabs.tabs_are_windows: default: false type: Bool desc: Whether to open windows instead of tabs. -title_format: +tabs.title.format: default: '{index}: {title}' type: name: FormatString @@ -783,7 +785,7 @@ title_format: * `{backend}`: Either ''webkit'' or ''webengine'' * `{private}` : Indicates when private mode is enabled. -title_format_pinned: +tabs.title.format_pinned: default: '{index}' type: name: FormatString @@ -801,17 +803,17 @@ title_format_pinned: desc: The format to use for the tab title for pinned tabs. The same placeholders like for title-format are defined. -title_alignment: +tabs.title.alignment: default: left type: TextAlignment desc: Alignment of the text inside of tabs -mousewheel_tab_switching: +tabs.mousewheel_switching: default: true type: Bool desc: Switch between tabs using the mouse wheel. -padding: +tabs.padding: default: top: 0 bottom: 0 @@ -820,7 +822,7 @@ padding: type: Padding desc: Padding for tabs -indicator_padding: +tabs.indicator_padding: default: top: 2 bottom: 2 @@ -832,7 +834,7 @@ indicator_padding: # storage -download_directory: +downloads.location.directory: default: "" type: name: Directory @@ -840,7 +842,7 @@ download_directory: desc: The directory to save downloads to. An empty value selects a sensible os-specific default. Will expand environment variables. -prompt_download_directory: +downloads.location.prompt: default: true type: Bool desc: >- @@ -848,7 +850,7 @@ prompt_download_directory: If set to false, `download-directory` will be used. -remember_download_directory: +downloads.location.remember: default: true type: Bool desc: Whether to remember the last used download directory. @@ -856,7 +858,7 @@ remember_download_directory: # Defaults from QWebSettings::QWebSettings() in # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp -maximum_pages_in_cache: +content.maximum_pages_in_cache: default: 0 type: name: Int @@ -871,7 +873,7 @@ maximum_pages_in_cache: For more information about the feature, please refer to: http://webkit.org/blog/427/webkit-page-cache-i-the-basics/ -offline_web_application_cache: +content.offline_web_application_cache: default: true type: Bool backend: QtWebKit @@ -884,12 +886,12 @@ offline_web_application_cache: The feature is described in details at: http://dev.w3.org/html5/spec/Overview.html#appcache -local_storage: +content.local_storage: default: true type: Bool desc: Whether support for HTML 5 local storage and Web SQL is enabled. -cache_size: +content.cache_size: default: null type: name: Int @@ -901,60 +903,60 @@ cache_size: # content -allow_images: +content.images: default: true type: Bool desc: Whether images are automatically loaded in web pages. -allow_javascript: +content.javascript: default: true type: Bool desc: Enables or disables the running of JavaScript programs. -allow_plugins: +content.plugins: default: false type: Bool desc: Enables or disables plugins in Web pages. -webgl: +content.webgl: default: true type: Bool desc: Enables or disables WebGL. -hyperlink_auditing: +content.hyperlink_auditing: default: false type: Bool desc: Enable or disable hyperlink auditing (``). -geolocation: +content.geolocation: default: ask type: BoolAsk desc: Allow websites to request geolocations. -notifications: +content.notifications: default: ask type: BoolAsk desc: Allow websites to show notifications. -media_capture: +content.media_capture: default: ask type: BoolAsk backend: QtWebEngine desc: Allow websites to record audio/video. -javascript_can_open_windows_automatically: +content.javascript.can_open_windows_automatically: default: false type: Bool desc: Whether JavaScript programs can open new windows without user interaction. -javascript_can_close_windows: +content.javascript.can_close_windows: default: false type: Bool backend: QtWebKit desc: Whether JavaScript programs can close windows. -javascript_can_access_clipboard: +content.javascript.can_access_clipboard: default: false type: Bool desc: >- @@ -963,27 +965,29 @@ javascript_can_access_clipboard: With QtWebEngine, writing the clipboard as response to a user interaction is always allowed. -ignore_javascript_prompt: +content.javascript.prompt: + # FIXME:conf meaning changed! + default: true + type: Bool + desc: Show javascript prompts. + +content.javascript.alert: + # FIXME:conf meaning changed! default: false type: Bool - desc: Whether all javascript prompts should be ignored. + desc: Show javascript. -ignore_javascript_alert: - default: false - type: Bool - desc: Whether all javascript alerts should be ignored. - -local_content_can_access_remote_urls: +content.local_content_can_access_remote_urls: default: false type: Bool desc: Whether locally loaded documents are allowed to access remote urls. -local_content_can_access_file_urls: +content.local_content_can_access_file_urls: default: true type: Bool desc: Whether locally loaded documents are allowed to access other local urls. -cookies_accept: +content.cookies.accept: default: no-3rdparty backend: QtWebKit type: @@ -996,13 +1000,13 @@ cookies_accept: - never: "Don't accept cookies at all." desc: Control which cookies to accept. -cookies_store: +cookies.cookies.store: default: true type: Bool desc: Whether to store cookies. Note this option needs a restart with QtWebEngine on Qt < 5.9. -host_block_lists: +content.host_blocking.lists: default: - "https://www.malwaredomainlist.com/hostslist/hosts.txt" - "http://someonewhocares.org/hosts/hosts" @@ -1023,12 +1027,12 @@ host_block_lists: - A zip-file of any of the above, with either only one file, or a file named `hosts` (with any extension). -host_blocking_enabled: +content.host_blocking.enabled: default: true type: Bool desc: Whether host blocking is enabled. -host_blocking_whitelist: +content.host_blocking.whitelist: default: - piwik.org type: @@ -1043,7 +1047,7 @@ host_blocking_whitelist: Local domains are always exempt from hostblocking. -enable_pdfjs: +content.pdfjs: default: false type: Bool desc: >- @@ -1055,12 +1059,12 @@ enable_pdfjs: # hints -border: +hints.border: default: '1px solid #E3BE23' type: String desc: CSS border value for hints. -mode: +hints.mode: default: letter type: name: String @@ -1072,7 +1076,7 @@ mode: - word: Use hints words based on the html elements and the extra words. desc: Mode to use for hints. -chars: +hints.chars: default: asdfghjkl type: name: UniqueCharString @@ -1083,32 +1087,32 @@ chars: - ['abcdefghijklmnopqrstuvwxyz', "All letters"] desc: Chars used for hint strings. -min_chars: +hints.min_chars: default: 1 type: name: Int minval: 1 desc: Minimum number of chars used for hint strings. -scatter: +hints.scatter: default: true type: Bool desc: Whether to scatter hint key chains (like Vimium) or not (like dwb). Ignored for number hints. -uppercase: +hints.uppercase: default: false type: Bool desc: Make chars in hint strings uppercase. -dictionary: +hints.dictionary: default: /usr/share/dict/words type: name: File required: false desc: The dictionary file to be used by the word hints. -auto_follow: +hints.auto_follow: default: unique-match type: name: String @@ -1122,13 +1126,13 @@ auto_follow: desc: Controls when a hint can be automatically followed without the user pressing Enter. -auto_follow_timeout: +hints.auto_follow_timeout: default: 0 type: Int desc: A timeout (in milliseconds) to inhibit normal-mode key bindings after a successful auto-follow. -next_regexes: +hints.next_regexes: default: - "\\bnext\\b" - "\\bmore\\b" @@ -1143,7 +1147,7 @@ next_regexes: flags: IGNORECASE desc: "A comma-separated list of regexes to use for 'next' links." -prev_regexes: +hints.prev_regexes: default: - "\\bprev(ious)?\\b" - "\\bback\\b" @@ -1157,7 +1161,7 @@ prev_regexes: flags: IGNORECASE desc: A comma-separated list of regexes to use for 'prev' links. -find_implementation: +hints.find_implementation: default: python type: name: String @@ -1166,7 +1170,7 @@ find_implementation: - python: Slightly worse but faster desc: Which implementation to use to find elements to hint. -hide_unmatched_rapid_hints: +hints.hide_unmatched_rapid_hints: default: true type: Bool desc: Controls hiding unmatched hints in rapid mode. @@ -1266,12 +1270,12 @@ colors.completion.scrollbar.bg: type: QssColor desc: Color of the scrollbar in completion view -colors.statusbar.fg: +colors.statusbar.fg.normal: default: white type: QssColor desc: Foreground color of the statusbar. -colors.statusbar.bg: +colors.statusbar.bg.normal: default: black type: QssColor desc: Background color of the statusbar. @@ -1306,12 +1310,12 @@ colors.statusbar.bg.command: type: QssColor desc: Background color of the statusbar in command mode. -colors.statusbar.fg.command.private: +colors.statusbar.fg.command_private: default: white type: QssColor desc: Foreground color of the statusbar in private browsing + command mode. -colors.statusbar.bg.command.private: +colors.statusbar.bg.command_private: default: grey type: QssColor desc: Background color of the statusbar in private browsing + command mode. @@ -1346,12 +1350,12 @@ colors.statusbar.url.fg: type: QssColor desc: Default foreground color of the URL in the statusbar. -colors.statusbar.url.fg.success: +colors.statusbar.url.success.http.fg: default: white type: QssColor desc: Foreground color of the URL in the statusbar on successful load (http). -colors.statusbar.url.fg.success.https: +colors.statusbar.url.success.https.bg: default: lime type: QssColor desc: Foreground color of the URL in the statusbar on successful load (https). @@ -1448,7 +1452,7 @@ colors.hints.bg: desc: Background color for hints. Note that you can use a `rgba(...)` value for transparency. -colors.hints.fg.match: +colors.hints.match.fg: default: green type: QssColor desc: Font color for the matched part of hints. @@ -1510,7 +1514,7 @@ colors.keyhint.fg: type: QssColor desc: Text color for the keyhint widget. -colors.keyhint.fg.suffix: +colors.keyhint.suffix.fg: default: '#FFFF00' type: CssColor desc: Highlight color for keys to complete the current keychain @@ -1590,7 +1594,7 @@ fonts.monospace: type: Font desc: Default monospace fonts. -fonts.completion: +fonts.completion.entry: default: 8pt monospace type: Font desc: Font used in the completion widget. @@ -1625,42 +1629,42 @@ fonts.debug_console: type: QtFont desc: Font used for the debugging console. -fonts.web_family_standard: +fonts.web.family.standard: default: '' type: name: FontFamily none_ok: true desc: Font family for standard fonts. -fonts.web_family_fixed: +fonts.web.family.fixed: default: '' type: name: FontFamily none_ok: true desc: Font family for fixed fonts. -fonts.web_family_serif: +fonts.web.family.serif: default: '' type: name: FontFamily none_ok: true desc: Font family for serif fonts. -fonts.web_family_sans_serif: +fonts.web.family.sans_serif: default: '' type: name: FontFamily none_ok: true desc: Font family for sans-serif fonts. -fonts.web_family_cursive: +fonts.web.family.cursive: default: '' type: name: FontFamily none_ok: true desc: Font family for cursive fonts. -fonts.web_family_fantasy: +fonts.web.family.fantasy: default: '' type: name: FontFamily @@ -1672,7 +1676,7 @@ fonts.web_family_fantasy: # QWebSettings::QWebSettings() in # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp -fonts.web_size_minimum: +fonts.web.size.minimum: default: 0 type: name: Int @@ -1680,7 +1684,7 @@ fonts.web_size_minimum: maxval: maxint desc: The hard minimum font size. -fonts.web_size_minimum_logical: +fonts.web.size.minimum_logical: # This is 0 as default on QtWebKit, and 6 on QtWebEngine - so let's # just go for 6 here. default: 6 @@ -1690,7 +1694,7 @@ fonts.web_size_minimum_logical: maxval: maxint desc: The minimum logical font size that is applied when zooming out. -fonts.web_size_default: +fonts.web.size.default: default: 16 type: name: Int @@ -1698,7 +1702,7 @@ fonts.web_size_default: maxval: maxint desc: The default font size for regular text. -fonts.web_size_default_fixed: +fonts.web.size.default_fixed: default: 13 type: name: Int From 52f6ea2525c97b1aaf7983bf959b50f2a6eca3fd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 9 Jun 2017 17:21:36 +0200 Subject: [PATCH 004/516] Initial parsing --- qutebrowser/config/config.py | 10 +- qutebrowser/config/configdata.py | 1496 ++--------------- qutebrowser/config/configdata.yml | 20 +- qutebrowser/config/configtypes.py | 64 +- qutebrowser/config/newconfig.py | 14 +- .../dev/pylint_checkers/qute_pylint/config.py | 7 +- tests/unit/config/test_configdata.py | 203 ++- 7 files changed, 405 insertions(+), 1409 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index fe3beb545..a723d2f10 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -73,10 +73,11 @@ class change_filter: # pylint: disable=invalid-name optname: The option to be filtered. function: Whether a function rather than a method is decorated. """ - if sectname not in configdata.DATA: - raise configexc.NoSectionError(sectname) - if optname is not None and optname not in configdata.DATA[sectname]: - raise configexc.NoOptionError(optname, sectname) + # FIXME:conf + # if sectname not in configdata.DATA: + # raise configexc.NoSectionError(sectname) + # if optname is not None and optname not in configdata.DATA[sectname]: + # raise configexc.NoOptionError(optname, sectname) self._sectname = sectname self._optname = optname self._function = function @@ -256,6 +257,7 @@ def init(parent=None): parent: The parent to pass to QObjects which get initialized. """ # _init_main_config(parent) + configdata.init() _init_new_config(parent) _init_key_config(parent) _init_misc() diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 711fca495..db1a9ce34 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -33,11 +33,12 @@ import sys import re import collections -from qutebrowser.config import configtypes as typ -from qutebrowser.config import sections as sect -from qutebrowser.config.value import SettingValue -from qutebrowser.utils.qtutils import MAXVALS -from qutebrowser.utils import usertypes, qtutils +import yaml + +from qutebrowser.config import configtypes, sections +from qutebrowser.utils import usertypes, qtutils, utils + +DATA = None FIRST_COMMENT = r""" @@ -131,1343 +132,6 @@ MONOSPACE = (' xos4 Terminus, Terminus, Monospace, ' 'monospace, Fixed, Consolas, Terminal') - -def data(readonly=False): - """Get the default config data. - - Return: - A {name: section} OrderedDict. - """ - return collections.OrderedDict([ - ('general', sect.KeyValue( - ('ignore-case', - SettingValue(typ.IgnoreCase(), 'smart'), - "Whether to find text on a page case-insensitively."), - - ('startpage', - SettingValue(typ.List(typ.String()), - 'https://start.duckduckgo.com'), - "The default page(s) to open at the start, separated by commas."), - - ('yank-ignored-url-parameters', - SettingValue(typ.List(typ.String()), - 'ref,utm_source,utm_medium,utm_campaign,utm_term,' - 'utm_content'), - "The URL parameters to strip with :yank url, separated by " - "commas."), - - ('default-open-dispatcher', - SettingValue(typ.String(none_ok=True), ''), - "The default program used to open downloads. Set to an empty " - "string to use the default internal handler.\n\n" - "Any {} in the string will be expanded to the filename, else " - "the filename will be appended."), - - ('default-page', - SettingValue(typ.FuzzyUrl(), 'https://start.duckduckgo.com/'), - "The page to open if :open -t/-b/-w is used without URL. Use " - "`about:blank` for a blank page."), - - ('auto-search', - SettingValue(typ.AutoSearch(), 'naive'), - "Whether to start a search when something else than a URL is " - "entered."), - - ('auto-save-config', - SettingValue(typ.Bool(), 'true'), - "Whether to save the config automatically on quit."), - - ('auto-save-interval', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '15000'), - "How often (in milliseconds) to auto-save config/cookies/etc."), - - ('editor', - SettingValue(typ.ShellCommand(placeholder=True), 'gvim -f "{}"'), - "The editor (and arguments) to use for the `open-editor` " - "command.\n\n" - "The arguments get split like in a shell, so you can use `\"` or " - "`'` to quote them.\n" - "`{}` gets replaced by the filename of the file to be edited."), - - ('editor-encoding', - SettingValue(typ.Encoding(), 'utf-8'), - "Encoding to use for editor."), - - ('private-browsing', - SettingValue(typ.Bool(), 'false'), - "Open new windows in private browsing mode which does not record " - "visited pages."), - - ('developer-extras', - SettingValue(typ.Bool(), 'false', - backends=[usertypes.Backend.QtWebKit]), - "Enable extra tools for Web developers.\n\n" - "This needs to be enabled for `:inspector` to work and also adds " - "an _Inspect_ entry to the context menu. For QtWebEngine, see " - "'qutebrowser --help' instead."), - - ('print-element-backgrounds', - SettingValue(typ.Bool(), 'true', - backends=( - None if qtutils.version_check('5.8', strict=True) - else [usertypes.Backend.QtWebKit])), - "Whether the background color and images are also drawn when the " - "page is printed.\n" - "This setting only works with Qt 5.8 or newer when using the " - "QtWebEngine backend."), - - ('xss-auditing', - SettingValue(typ.Bool(), 'false'), - "Whether load requests should be monitored for cross-site " - "scripting attempts.\n\n" - "Suspicious scripts will be blocked and reported in the " - "inspector's JavaScript console. Enabling this feature might " - "have an impact on performance."), - - ('default-encoding', - SettingValue(typ.String(), 'iso-8859-1'), - "Default encoding to use for websites.\n\n" - "The encoding must be a string describing an encoding such as " - "_utf-8_, _iso-8859-1_, etc."), - - ('new-instance-open-target', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('tab', "Open a new tab in the existing " - "window and activate the window."), - ('tab-bg', "Open a new background tab in the " - "existing window and activate the " - "window."), - ('tab-silent', "Open a new tab in the existing " - "window without activating " - "the window."), - ('tab-bg-silent', "Open a new background tab " - "in the existing window " - "without activating the " - "window."), - ('window', "Open in a new window.") - )), 'tab'), - "How to open links in an existing instance if a new one is " - "launched."), - - ('new-instance-open-target.window', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('first-opened', "Open new tabs in the first (oldest) " - "opened window."), - ('last-opened', "Open new tabs in the last (newest) " - "opened window."), - ('last-focused', "Open new tabs in the most recently " - "focused window."), - ('last-visible', "Open new tabs in the most recently " - "visible window.") - )), 'last-focused'), - "Which window to choose when opening links as new tabs."), - - ('log-javascript-console', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('none', "Don't log messages."), - ('debug', "Log messages with debug level."), - ('info', "Log messages with info level.") - )), 'debug'), - "How to log javascript console messages."), - - ('save-session', - SettingValue(typ.Bool(), 'false'), - "Whether to always save the open pages."), - - ('session-default-name', - SettingValue(typ.SessionName(none_ok=True), ''), - "The name of the session to save by default, or empty for the " - "last loaded session."), - - ('url-incdec-segments', - SettingValue( - typ.FlagList(valid_values=typ.ValidValues( - 'host', 'path', 'query', 'anchor')), - 'path,query'), - "The URL segments where `:navigate increment/decrement` will " - "search for a number."), - - readonly=readonly - )), - - ('ui', sect.KeyValue( - ('history-session-interval', - SettingValue(typ.Int(), '30'), - "The maximum time in minutes between two history items for them " - "to be considered being from the same session. Use -1 to " - "disable separation."), - - ('zoom-levels', - SettingValue(typ.List(typ.Perc(minval=0)), - '25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,' - '200%,250%,300%,400%,500%'), - "The available zoom levels, separated by commas."), - - ('default-zoom', - SettingValue(typ.Perc(), '100%'), - "The default zoom level."), - - ('downloads-position', - SettingValue(typ.VerticalPosition(), 'top'), - "Where to show the downloaded files."), - - ('status-position', - SettingValue(typ.VerticalPosition(), 'bottom'), - "The position of the status bar."), - - ('message-timeout', - SettingValue(typ.Int(minval=0), '2000'), - "Time (in ms) to show messages in the statusbar for.\n" - "Set to 0 to never clear messages."), - - ('message-unfocused', - SettingValue(typ.Bool(), 'false'), - "Whether to show messages in unfocused windows."), - - ('confirm-quit', - SettingValue(typ.ConfirmQuit(), 'never'), - "Whether to confirm quitting the application."), - - ('zoom-text-only', - SettingValue(typ.Bool(), 'false', - backends=[usertypes.Backend.QtWebKit]), - "Whether the zoom factor on a frame applies only to the text or " - "to all content."), - - ('frame-flattening', - SettingValue(typ.Bool(), 'false', - backends=[usertypes.Backend.QtWebKit]), - "Whether to expand each subframe to its contents.\n\n" - "This will flatten all the frames to become one scrollable " - "page."), - - ('user-stylesheet', - SettingValue(typ.File(none_ok=True), ''), - "User stylesheet to use (absolute filename or filename relative " - "to the config directory). Will expand environment variables."), - - ('hide-scrollbar', - SettingValue(typ.Bool(), 'true'), - "Hide the main scrollbar."), - - ('smooth-scrolling', - SettingValue(typ.Bool(), 'false'), - "Whether to enable smooth scrolling for web pages. Note smooth " - "scrolling does not work with the :scroll-px command."), - - ('remove-finished-downloads', - SettingValue(typ.Int(minval=-1), '-1'), - "Number of milliseconds to wait before removing finished " - "downloads. Will not be removed if value is -1."), - - ('hide-statusbar', - SettingValue(typ.Bool(), 'false'), - "Whether to hide the statusbar unless a message is shown."), - - ('statusbar-padding', - SettingValue(typ.Padding(), '1,1,0,0'), - "Padding for statusbar (top, bottom, left, right)."), - - ('window-title-format', - SettingValue(typ.FormatString(fields=['perc', 'perc_raw', 'title', - 'title_sep', 'id', - 'scroll_pos', 'host', - 'backend', 'private']), - '{perc}{title}{title_sep}qutebrowser'), - "The format to use for the window title. The following " - "placeholders are defined:\n\n" - "* `{perc}`: The percentage as a string like `[10%]`.\n" - "* `{perc_raw}`: The raw percentage, e.g. `10`\n" - "* `{title}`: The title of the current web page\n" - "* `{title_sep}`: The string ` - ` if a title is set, empty " - "otherwise.\n" - "* `{id}`: The internal window ID of this window.\n" - "* `{scroll_pos}`: The page scroll position.\n" - "* `{host}`: The host of the current web page.\n" - "* `{backend}`: Either 'webkit' or 'webengine'\n" - "* `{private}` : Indicates when private mode is enabled.\n"), - - ('modal-js-dialog', - SettingValue(typ.Bool(), 'false'), - "Use standard JavaScript modal dialog for alert() and confirm()"), - - ('hide-wayland-decoration', - SettingValue(typ.Bool(), 'false'), - "Hide the window decoration when using wayland " - "(requires restart)"), - - ('keyhint-blacklist', - SettingValue(typ.List(typ.String(), none_ok=True), ''), - "Keychains that shouldn't be shown in the keyhint dialog\n\n" - "Globs are supported, so ';*' will blacklist all keychains" - "starting with ';'. Use '*' to disable keyhints"), - - ('keyhint-delay', - SettingValue(typ.Int(minval=0), '500'), - "Time from pressing a key to seeing the keyhint dialog (ms)"), - - ('prompt-radius', - SettingValue(typ.Int(minval=0), '8'), - "The rounding radius for the edges of prompts."), - - ('prompt-filebrowser', - SettingValue(typ.Bool(), 'true'), - "Show a filebrowser in upload/download prompts."), - - readonly=readonly - )), - - ('network', sect.KeyValue( - ('do-not-track', - SettingValue(typ.Bool(), 'true'), - "Value to send in the `DNT` header."), - - ('accept-language', - SettingValue(typ.String(none_ok=True), 'en-US,en'), - "Value to send in the `accept-language` header."), - - ('referer-header', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('always', "Always send."), - ('never', "Never send; this is not recommended," - " as some sites may break."), - ('same-domain', "Only send for the same domain." - " This will still protect your privacy, but" - " shouldn't break any sites.") - )), 'same-domain', backends=[usertypes.Backend.QtWebKit]), - "Send the Referer header"), - - ('user-agent', - SettingValue(typ.UserAgent(none_ok=True), ''), - "User agent to send. Empty to send the default."), - - ('proxy', - SettingValue(typ.Proxy(), 'system', - backends=(None if qtutils.version_check('5.8') - else [usertypes.Backend.QtWebKit])), - "The proxy to use.\n\n" - "In addition to the listed values, you can use a `socks://...` " - "or `http://...` URL.\n\n" - "This setting only works with Qt 5.8 or newer when using the " - "QtWebEngine backend."), - - ('proxy-dns-requests', - SettingValue(typ.Bool(), 'true', - backends=[usertypes.Backend.QtWebKit]), - "Whether to send DNS requests over the configured proxy."), - - ('ssl-strict', - SettingValue(typ.BoolAsk(), 'ask'), - "Whether to validate SSL handshakes."), - - ('dns-prefetch', - SettingValue(typ.Bool(), 'true', - backends=[usertypes.Backend.QtWebKit]), - "Whether to try to pre-fetch DNS entries to speed up browsing."), - - ('custom-headers', - SettingValue(typ.HeaderDict(none_ok=True), ''), - "Set custom headers for qutebrowser HTTP requests."), - - ('netrc-file', - SettingValue(typ.File(none_ok=True), ''), - "Set location of a netrc-file for HTTP authentication. If empty, " - "~/.netrc is used."), - - readonly=readonly - )), - - ('completion', sect.KeyValue( - ('show', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('always', "Whenever a completion is available."), - ('auto', "Whenever a completion is requested."), - ('never', "Never.") - )), 'always'), - "When to show the autocompletion window."), - - ('download-path-suggestion', - SettingValue( - typ.String(valid_values=typ.ValidValues( - ('path', "Show only the download path."), - ('filename', "Show only download filename."), - ('both', "Show download path and filename."))), - 'path'), - "What to display in the download filename input."), - - ('timestamp-format', - SettingValue(typ.TimestampTemplate(none_ok=True), '%Y-%m-%d'), - "How to format timestamps (e.g. for history)"), - - ('height', - SettingValue(typ.PercOrInt(minperc=0, maxperc=100, minint=1), - '50%'), - "The height of the completion, in px or as percentage of the " - "window."), - - ('cmd-history-max-items', - SettingValue(typ.Int(minval=-1), '100'), - "How many commands to save in the command history.\n\n" - "0: no history / -1: unlimited"), - - ('web-history-max-items', - SettingValue(typ.Int(minval=-1), '1000'), - "How many URLs to show in the web history.\n\n" - "0: no history / -1: unlimited"), - - ('quick-complete', - SettingValue(typ.Bool(), 'true'), - "Whether to move on to the next part when there's only one " - "possible completion left."), - - ('shrink', - SettingValue(typ.Bool(), 'false'), - "Whether to shrink the completion to be smaller than the " - "configured size if there are no scrollbars."), - - ('scrollbar-width', - SettingValue(typ.Int(minval=0), '12'), - "Width of the scrollbar in the completion window (in px)."), - - ('scrollbar-padding', - SettingValue(typ.Int(minval=0), '2'), - "Padding of scrollbar handle in completion window (in px)."), - - readonly=readonly - )), - - ('input', sect.KeyValue( - ('timeout', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '500'), - "Timeout (in milliseconds) for ambiguous key bindings.\n\n" - "If the current input forms both a complete match and a partial " - "match, the complete match will be executed after this time."), - - ('partial-timeout', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '5000'), - "Timeout (in milliseconds) for partially typed key bindings.\n\n" - "If the current input forms only partial matches, the keystring " - "will be cleared after this time."), - - ('insert-mode-on-plugins', - SettingValue(typ.Bool(), 'false'), - "Whether to switch to insert mode when clicking flash and other " - "plugins."), - - ('auto-leave-insert-mode', - SettingValue(typ.Bool(), 'true'), - "Whether to leave insert mode if a non-editable element is " - "clicked."), - - ('auto-insert-mode', - SettingValue(typ.Bool(), 'false'), - "Whether to automatically enter insert mode if an editable " - "element is focused after page load."), - - ('forward-unbound-keys', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('all', "Forward all unbound keys."), - ('auto', "Forward unbound non-alphanumeric " - "keys."), - ('none', "Don't forward any keys.") - )), 'auto'), - "Whether to forward unbound keys to the webview in normal mode."), - - ('spatial-navigation', - SettingValue(typ.Bool(), 'false'), - "Enables or disables the Spatial Navigation feature.\n\n" - "Spatial navigation consists in the ability to navigate between " - "focusable elements in a Web page, such as hyperlinks and form " - "controls, by using Left, Right, Up and Down arrow keys. For " - "example, if a user presses the Right key, heuristics determine " - "whether there is an element he might be trying to reach towards " - "the right and which element he probably wants."), - - ('links-included-in-focus-chain', - SettingValue(typ.Bool(), 'true'), - "Whether hyperlinks should be included in the keyboard focus " - "chain."), - - ('rocker-gestures', - SettingValue(typ.Bool(), 'false'), - "Whether to enable Opera-like mouse rocker gestures. This " - "disables the context menu."), - - ('mouse-zoom-divider', - SettingValue(typ.Int(minval=0), '512'), - "How much to divide the mouse wheel movements to translate them " - "into zoom increments."), - - readonly=readonly - )), - - ('tabs', sect.KeyValue( - ('background-tabs', - SettingValue(typ.Bool(), 'false'), - "Whether to open new tabs (middleclick/ctrl+click) in " - "background."), - - ('select-on-remove', - SettingValue(typ.SelectOnRemove(), 'next'), - "Which tab to select when the focused tab is removed."), - - ('new-tab-position', - SettingValue(typ.NewTabPosition(), 'next'), - "How new tabs are positioned."), - - ('new-tab-position-explicit', - SettingValue(typ.NewTabPosition(), 'last'), - "How new tabs opened explicitly are positioned."), - - ('last-close', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('ignore', "Don't do anything."), - ('blank', "Load a blank page."), - ('startpage', "Load the start page."), - ('default-page', "Load the default page."), - ('close', "Close the window.") - )), 'ignore'), - "Behavior when the last tab is closed."), - - ('show', - SettingValue( - typ.String(valid_values=typ.ValidValues( - ('always', "Always show the tab bar."), - ('never', "Always hide the tab bar."), - ('multiple', "Hide the tab bar if only one tab " - "is open."), - ('switching', "Show the tab bar when switching " - "tabs.") - )), 'always'), - "When to show the tab bar"), - - ('show-switching-delay', - SettingValue(typ.Int(), '800'), - "Time to show the tab bar before hiding it when tabs->show is " - "set to 'switching'."), - - ('wrap', - SettingValue(typ.Bool(), 'true'), - "Whether to wrap when changing tabs."), - - ('movable', - SettingValue(typ.Bool(), 'true'), - "Whether tabs should be movable."), - - ('close-mouse-button', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('right', "Close tabs on right-click."), - ('middle', "Close tabs on middle-click."), - ('none', "Don't close tabs using the mouse.") - )), 'middle'), - "On which mouse button to close tabs."), - - ('position', - SettingValue(typ.Position(), 'top'), - "The position of the tab bar."), - - ('show-favicons', - SettingValue(typ.Bool(), 'true'), - "Whether to show favicons in the tab bar."), - - ('favicon-scale', - SettingValue(typ.Float(minval=0.0), '1.0'), - "Scale for favicons in the tab bar. The tab size is unchanged, " - "so big favicons also require extra `tabs->padding`."), - - ('width', - SettingValue(typ.PercOrInt(minperc=0, maxperc=100, minint=1), - '20%'), - "The width of the tab bar if it's vertical, in px or as " - "percentage of the window."), - - ('pinned-width', - SettingValue(typ.Int(minval=10), - '43'), - "The width for pinned tabs with a horizontal tabbar, in px."), - - ('indicator-width', - SettingValue(typ.Int(minval=0), '3'), - "Width of the progress indicator (0 to disable)."), - - ('tabs-are-windows', - SettingValue(typ.Bool(), 'false'), - "Whether to open windows instead of tabs."), - - ('title-format', - SettingValue(typ.FormatString( - fields=['perc', 'perc_raw', 'title', 'title_sep', 'index', - 'id', 'scroll_pos', 'host', 'private'], none_ok=True), - '{index}: {title}'), - "The format to use for the tab title. The following placeholders " - "are defined:\n\n" - "* `{perc}`: The percentage as a string like `[10%]`.\n" - "* `{perc_raw}`: The raw percentage, e.g. `10`\n" - "* `{title}`: The title of the current web page\n" - "* `{title_sep}`: The string ` - ` if a title is set, empty " - "otherwise.\n" - "* `{index}`: The index of this tab.\n" - "* `{id}`: The internal tab ID of this tab.\n" - "* `{scroll_pos}`: The page scroll position.\n" - "* `{host}`: The host of the current web page.\n" - "* `{backend}`: Either 'webkit' or 'webengine'\n" - "* `{private}` : Indicates when private mode is enabled.\n"), - - ('title-format-pinned', - SettingValue(typ.FormatString( - fields=['perc', 'perc_raw', 'title', 'title_sep', 'index', - 'id', 'scroll_pos', 'host', 'private'], none_ok=True), - '{index}'), - "The format to use for the tab title for pinned tabs. " - "The same placeholders like for title-format are defined."), - - ('title-alignment', - SettingValue(typ.TextAlignment(), 'left'), - "Alignment of the text inside of tabs"), - - ('mousewheel-tab-switching', - SettingValue(typ.Bool(), 'true'), - "Switch between tabs using the mouse wheel."), - - ('padding', - SettingValue(typ.Padding(), '0,0,5,5'), - "Padding for tabs (top, bottom, left, right)."), - - ('indicator-padding', - SettingValue(typ.Padding(), '2,2,0,4'), - "Padding for indicators (top, bottom, left, right)."), - - readonly=readonly - )), - - ('storage', sect.KeyValue( - ('download-directory', - SettingValue(typ.Directory(none_ok=True), ''), - "The directory to save downloads to. An empty value selects a " - "sensible os-specific default. Will expand environment " - "variables."), - - ('prompt-download-directory', - SettingValue(typ.Bool(), 'true'), - "Whether to prompt the user for the download location.\n" - "If set to false, 'download-directory' will be used."), - - ('remember-download-directory', - SettingValue(typ.Bool(), 'true'), - "Whether to remember the last used download directory."), - - # Defaults from QWebSettings::QWebSettings() in - # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp - - ('maximum-pages-in-cache', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '0', - backends=[usertypes.Backend.QtWebKit]), - "The maximum number of pages to hold in the global memory page " - "cache.\n\n" - "The Page Cache allows for a nicer user experience when " - "navigating forth or back to pages in the forward/back history, " - "by pausing and resuming up to _n_ pages.\n\n" - "For more information about the feature, please refer to: " - "http://webkit.org/blog/427/webkit-page-cache-i-the-basics/"), - - ('offline-web-application-cache', - SettingValue(typ.Bool(), 'true', - backends=[usertypes.Backend.QtWebKit]), - "Whether support for the HTML 5 web application cache feature is " - "enabled.\n\n" - "An application cache acts like an HTTP cache in some sense. For " - "documents that use the application cache via JavaScript, the " - "loader engine will first ask the application cache for the " - "contents, before hitting the network.\n\n" - "The feature is described in details at: " - "http://dev.w3.org/html5/spec/Overview.html#appcache"), - - ('local-storage', - SettingValue(typ.Bool(), 'true'), - "Whether support for HTML 5 local storage and Web SQL is " - "enabled."), - - ('cache-size', - SettingValue(typ.Int(none_ok=True, minval=0, - maxval=MAXVALS['int64']), ''), - "Size of the HTTP network cache. Empty to use the default " - "value."), - - readonly=readonly - )), - - ('content', sect.KeyValue( - ('allow-images', - SettingValue(typ.Bool(), 'true'), - "Whether images are automatically loaded in web pages."), - - ('allow-javascript', - SettingValue(typ.Bool(), 'true'), - "Enables or disables the running of JavaScript programs."), - - ('allow-plugins', - SettingValue(typ.Bool(), 'false'), - "Enables or disables plugins in Web pages.\n\n" - 'Qt plugins with a mimetype such as "application/x-qt-plugin" ' - "are not affected by this setting."), - - ('webgl', - SettingValue(typ.Bool(), 'true'), - "Enables or disables WebGL."), - - ('hyperlink-auditing', - SettingValue(typ.Bool(), 'false'), - "Enable or disable hyperlink auditing ()."), - - ('geolocation', - SettingValue(typ.BoolAsk(), 'ask'), - "Allow websites to request geolocations."), - - ('notifications', - SettingValue(typ.BoolAsk(), 'ask'), - "Allow websites to show notifications."), - - ('media-capture', - SettingValue(typ.BoolAsk(), 'ask', - backends=[usertypes.Backend.QtWebEngine]), - "Allow websites to record audio/video."), - - ('javascript-can-open-windows-automatically', - SettingValue(typ.Bool(), 'false'), - "Whether JavaScript programs can open new windows without user " - "interaction."), - - ('javascript-can-close-windows', - SettingValue(typ.Bool(), 'false', - backends=[usertypes.Backend.QtWebKit]), - "Whether JavaScript programs can close windows."), - - ('javascript-can-access-clipboard', - SettingValue(typ.Bool(), 'false'), - "Whether JavaScript programs can read or write to the " - "clipboard.\nWith QtWebEngine, writing the clipboard as response " - "to a user interaction is always allowed."), - - ('ignore-javascript-prompt', - SettingValue(typ.Bool(), 'false'), - "Whether all javascript prompts should be ignored."), - - ('ignore-javascript-alert', - SettingValue(typ.Bool(), 'false'), - "Whether all javascript alerts should be ignored."), - - ('local-content-can-access-remote-urls', - SettingValue(typ.Bool(), 'false'), - "Whether locally loaded documents are allowed to access remote " - "urls."), - - ('local-content-can-access-file-urls', - SettingValue(typ.Bool(), 'true'), - "Whether locally loaded documents are allowed to access other " - "local urls."), - - ('cookies-accept', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('all', "Accept all cookies."), - ('no-3rdparty', "Accept cookies from the same" - " origin only."), - ('no-unknown-3rdparty', "Accept cookies from " - "the same origin only, unless a cookie is " - "already set for the domain."), - ('never', "Don't accept cookies at all.") - )), 'no-3rdparty', backends=[usertypes.Backend.QtWebKit]), - "Control which cookies to accept."), - - ('cookies-store', - SettingValue(typ.Bool(), 'true'), - "Whether to store cookies. Note this option needs a restart with " - "QtWebEngine on Qt < 5.9."), - - ('host-block-lists', - SettingValue( - typ.List(typ.Url(), none_ok=True), - 'https://www.malwaredomainlist.com/hostslist/hosts.txt,' - 'http://someonewhocares.org/hosts/hosts,' - 'http://winhelp2002.mvps.org/hosts.zip,' - 'http://malwaredomains.lehigh.edu/files/justdomains.zip,' - 'https://pgl.yoyo.org/adservers/serverlist.php?' - 'hostformat=hosts&mimetype=plaintext'), - "List of URLs of lists which contain hosts to block.\n\n" - "The file can be in one of the following formats:\n\n" - "- An '/etc/hosts'-like file\n" - "- One host per line\n" - "- A zip-file of any of the above, with either only one file, or " - "a file named 'hosts' (with any extension)."), - - ('host-blocking-enabled', - SettingValue(typ.Bool(), 'true'), - "Whether host blocking is enabled."), - - ('host-blocking-whitelist', - SettingValue(typ.List(typ.String(), none_ok=True), 'piwik.org'), - "List of domains that should always be loaded, despite being " - "ad-blocked.\n\n" - "Domains may contain * and ? wildcards and are otherwise " - "required to exactly match the requested domain.\n\n" - "Local domains are always exempt from hostblocking."), - - ('enable-pdfjs', SettingValue(typ.Bool(), 'false'), - "Enable pdf.js to view PDF files in the browser.\n\n" - "Note that the files can still be downloaded by clicking" - " the download button in the pdf.js viewer."), - - readonly=readonly - )), - - ('hints', sect.KeyValue( - ('border', - SettingValue(typ.String(), '1px solid #E3BE23'), - "CSS border value for hints."), - - ('mode', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('number', "Use numeric hints. (In this mode you can " - "also type letters form the hinted element to filter " - "and reduce the number of elements that are hinted.)"), - ('letter', "Use the chars in the hints -> " - "chars setting."), - ('word', "Use hints words based on the html " - "elements and the extra words."), - )), 'letter'), - "Mode to use for hints."), - - ('chars', - SettingValue(typ.UniqueCharString(minlen=2, completions=[ - ('asdfghjkl', "Home row"), - ('aoeuidnths', "Home row (Dvorak)"), - ('abcdefghijklmnopqrstuvwxyz', "All letters"), - ]), 'asdfghjkl'), - "Chars used for hint strings."), - - ('min-chars', - SettingValue(typ.Int(minval=1), '1'), - "Minimum number of chars used for hint strings."), - - ('scatter', - SettingValue(typ.Bool(), 'true'), - "Whether to scatter hint key chains (like Vimium) or not (like " - "dwb). Ignored for number hints."), - - ('uppercase', - SettingValue(typ.Bool(), 'false'), - "Make chars in hint strings uppercase."), - - ('dictionary', - SettingValue(typ.File(required=False), '/usr/share/dict/words'), - "The dictionary file to be used by the word hints."), - - ('auto-follow', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('always', "Auto-follow whenever there is only a single " - "hint on a page."), - ('unique-match', "Auto-follow whenever there is a unique " - "non-empty match in either the hint string (word mode) " - "or filter (number mode)."), - ('full-match', "Follow the hint when the user typed the " - "whole hint (letter, word or number mode) or the " - "element's text (only in number mode)."), - ('never', "The user will always need to press Enter to " - "follow a hint."), - )), 'unique-match'), - "Controls when a hint can be automatically followed without the " - "user pressing Enter."), - - ('auto-follow-timeout', - SettingValue(typ.Int(), '0'), - "A timeout (in milliseconds) to inhibit normal-mode key bindings " - "after a successful auto-follow."), - - ('next-regexes', - SettingValue(typ.List(typ.Regex(flags=re.IGNORECASE)), - r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,' - r'\bcontinue\b'), - "A comma-separated list of regexes to use for 'next' links."), - - ('prev-regexes', - SettingValue(typ.List(typ.Regex(flags=re.IGNORECASE)), - r'\bprev(ious)?\b,\bback\b,\bolder\b,\b[<←≪]\b,' - r'\b(<<|«)\b'), - "A comma-separated list of regexes to use for 'prev' links."), - - ('find-implementation', - SettingValue(typ.String( - valid_values=typ.ValidValues( - ('javascript', "Better but slower"), - ('python', "Slightly worse but faster"), - )), 'python'), - "Which implementation to use to find elements to hint."), - - ('hide-unmatched-rapid-hints', - SettingValue(typ.Bool(), 'true'), - "Controls hiding unmatched hints in rapid mode."), - - readonly=readonly - )), - - ('searchengines', sect.ValueList( - typ.SearchEngineName(), typ.SearchEngineUrl(), - ('DEFAULT', 'https://duckduckgo.com/?q={}'), - - readonly=readonly - )), - - ('aliases', sect.ValueList( - typ.String(forbidden=' '), typ.Command(), - - readonly=readonly - )), - - ('colors', sect.KeyValue( - ('completion.fg', - SettingValue(typ.QtColor(), 'white'), - "Text color of the completion widget."), - - ('completion.bg', - SettingValue(typ.QssColor(), '#333333'), - "Background color of the completion widget."), - - ('completion.alternate-bg', - SettingValue(typ.QssColor(), '#444444'), - "Alternating background color of the completion widget."), - - ('completion.category.fg', - SettingValue(typ.QtColor(), 'white'), - "Foreground color of completion widget category headers."), - - ('completion.category.bg', - SettingValue(typ.QssColor(), 'qlineargradient(x1:0, y1:0, x2:0, ' - 'y2:1, stop:0 #888888, stop:1 #505050)'), - "Background color of the completion widget category headers."), - - ('completion.category.border.top', - SettingValue(typ.QssColor(), 'black'), - "Top border color of the completion widget category headers."), - - ('completion.category.border.bottom', - SettingValue(typ.QssColor(), 'black'), - "Bottom border color of the completion widget category headers."), - - ('completion.item.selected.fg', - SettingValue(typ.QtColor(), 'black'), - "Foreground color of the selected completion item."), - - ('completion.item.selected.bg', - SettingValue(typ.QssColor(), '#e8c000'), - "Background color of the selected completion item."), - - ('completion.item.selected.border.top', - SettingValue(typ.QssColor(), '#bbbb00'), - "Top border color of the completion widget category headers."), - - ('completion.item.selected.border.bottom', - SettingValue( - typ.QssColor(), '#bbbb00'), - "Bottom border color of the selected completion item."), - - ('completion.match.fg', - SettingValue(typ.QssColor(), '#ff4444'), - "Foreground color of the matched text in the completion."), - - ('completion.scrollbar.fg', - SettingValue(typ.QssColor(), 'white'), - "Color of the scrollbar handle in completion view."), - - ('completion.scrollbar.bg', - SettingValue(typ.QssColor(), '#333333'), - "Color of the scrollbar in completion view"), - - ('statusbar.fg', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of the statusbar."), - - ('statusbar.bg', - SettingValue(typ.QssColor(), 'black'), - "Background color of the statusbar."), - - ('statusbar.fg.private', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of the statusbar in private browsing mode."), - - ('statusbar.bg.private', - SettingValue(typ.QssColor(), '#666666'), - "Background color of the statusbar in private browsing mode."), - - ('statusbar.fg.insert', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of the statusbar in insert mode."), - - ('statusbar.bg.insert', - SettingValue(typ.QssColor(), 'darkgreen'), - "Background color of the statusbar in insert mode."), - - ('statusbar.fg.command', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of the statusbar in command mode."), - - ('statusbar.bg.command', - SettingValue(typ.QssColor(), 'black'), - "Background color of the statusbar in command mode."), - - ('statusbar.fg.command.private', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of the statusbar in private browsing + command " - "mode."), - - ('statusbar.bg.command.private', - SettingValue(typ.QssColor(), 'grey'), - "Background color of the statusbar in private browsing + command " - "mode."), - - ('statusbar.fg.caret', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of the statusbar in caret mode."), - - ('statusbar.bg.caret', - SettingValue(typ.QssColor(), 'purple'), - "Background color of the statusbar in caret mode."), - - ('statusbar.fg.caret-selection', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of the statusbar in caret mode with a " - "selection"), - - ('statusbar.bg.caret-selection', - SettingValue(typ.QssColor(), '#a12dff'), - "Background color of the statusbar in caret mode with a " - "selection"), - - ('statusbar.progress.bg', - SettingValue(typ.QssColor(), 'white'), - "Background color of the progress bar."), - - ('statusbar.url.fg', - SettingValue(typ.QssColor(), 'white'), - "Default foreground color of the URL in the statusbar."), - - ('statusbar.url.fg.success', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of the URL in the statusbar on successful " - "load (http)."), - - ('statusbar.url.fg.success.https', - SettingValue(typ.QssColor(), 'lime'), - "Foreground color of the URL in the statusbar on successful " - "load (https)."), - - ('statusbar.url.fg.error', - SettingValue(typ.QssColor(), 'orange'), - "Foreground color of the URL in the statusbar on error."), - - ('statusbar.url.fg.warn', - SettingValue(typ.QssColor(), 'yellow'), - "Foreground color of the URL in the statusbar when there's a " - "warning."), - - ('statusbar.url.fg.hover', - SettingValue(typ.QssColor(), 'aqua'), - "Foreground color of the URL in the statusbar for hovered " - "links."), - - ('tabs.fg.odd', - SettingValue(typ.QtColor(), 'white'), - "Foreground color of unselected odd tabs."), - - ('tabs.bg.odd', - SettingValue(typ.QtColor(), 'grey'), - "Background color of unselected odd tabs."), - - ('tabs.fg.even', - SettingValue(typ.QtColor(), 'white'), - "Foreground color of unselected even tabs."), - - ('tabs.bg.even', - SettingValue(typ.QtColor(), 'darkgrey'), - "Background color of unselected even tabs."), - - ('tabs.fg.selected.odd', - SettingValue(typ.QtColor(), 'white'), - "Foreground color of selected odd tabs."), - - ('tabs.bg.selected.odd', - SettingValue(typ.QtColor(), 'black'), - "Background color of selected odd tabs."), - - ('tabs.fg.selected.even', - SettingValue(typ.QtColor(), 'white'), - "Foreground color of selected even tabs."), - - ('tabs.bg.selected.even', - SettingValue(typ.QtColor(), 'black'), - "Background color of selected even tabs."), - - ('tabs.bg.bar', - SettingValue(typ.QtColor(), '#555555'), - "Background color of the tab bar."), - - ('tabs.indicator.start', - SettingValue(typ.QtColor(), '#0000aa'), - "Color gradient start for the tab indicator."), - - ('tabs.indicator.stop', - SettingValue(typ.QtColor(), '#00aa00'), - "Color gradient end for the tab indicator."), - - ('tabs.indicator.error', - SettingValue(typ.QtColor(), '#ff0000'), - "Color for the tab indicator on errors.."), - - ('tabs.indicator.system', - SettingValue(typ.ColorSystem(), 'rgb'), - "Color gradient interpolation system for the tab indicator."), - - ('hints.fg', - SettingValue(typ.QssColor(), 'black'), - "Font color for hints."), - - ('hints.bg', - SettingValue(typ.QssColor(), 'qlineargradient(x1:0, y1:0, x2:0, ' - 'y2:1, stop:0 rgba(255, 247, 133, 0.8), ' - 'stop:1 rgba(255, 197, 66, 0.8))'), - "Background color for hints. Note that you can use a `rgba(...)` " - "value for transparency."), - - ('hints.fg.match', - SettingValue(typ.QssColor(), 'green'), - "Font color for the matched part of hints."), - - ('downloads.bg.bar', - SettingValue(typ.QssColor(), 'black'), - "Background color for the download bar."), - - ('downloads.fg.start', - SettingValue(typ.QtColor(), 'white'), - "Color gradient start for download text."), - - ('downloads.bg.start', - SettingValue(typ.QtColor(), '#0000aa'), - "Color gradient start for download backgrounds."), - - ('downloads.fg.stop', - SettingValue(typ.QtColor(), '#0000aa'), - "Color gradient end for download text."), - - ('downloads.bg.stop', - SettingValue(typ.QtColor(), '#00aa00'), - "Color gradient stop for download backgrounds."), - - ('downloads.fg.system', - SettingValue(typ.ColorSystem(), 'rgb'), - "Color gradient interpolation system for download text."), - - ('downloads.bg.system', - SettingValue(typ.ColorSystem(), 'rgb'), - "Color gradient interpolation system for download backgrounds."), - - ('downloads.fg.error', - SettingValue(typ.QtColor(), 'white'), - "Foreground color for downloads with errors."), - - ('downloads.bg.error', - SettingValue(typ.QtColor(), 'red'), - "Background color for downloads with errors."), - - ('webpage.bg', - SettingValue(typ.QtColor(none_ok=True), 'white'), - "Background color for webpages if unset (or empty to use the " - "theme's color)"), - - ('keyhint.fg', - SettingValue(typ.QssColor(), '#FFFFFF'), - "Text color for the keyhint widget."), - - ('keyhint.fg.suffix', - SettingValue(typ.CssColor(), '#FFFF00'), - "Highlight color for keys to complete the current keychain"), - - ('keyhint.bg', - SettingValue(typ.QssColor(), 'rgba(0, 0, 0, 80%)'), - "Background color of the keyhint widget."), - - ('messages.fg.error', - SettingValue(typ.QssColor(), 'white'), - "Foreground color of an error message."), - - ('messages.bg.error', - SettingValue(typ.QssColor(), 'red'), - "Background color of an error message."), - - ('messages.border.error', - SettingValue(typ.QssColor(), '#bb0000'), - "Border color of an error message."), - - ('messages.fg.warning', - SettingValue(typ.QssColor(), 'white'), - "Foreground color a warning message."), - - ('messages.bg.warning', - SettingValue(typ.QssColor(), 'darkorange'), - "Background color of a warning message."), - - ('messages.border.warning', - SettingValue(typ.QssColor(), '#d47300'), - "Border color of an error message."), - - ('messages.fg.info', - SettingValue(typ.QssColor(), 'white'), - "Foreground color an info message."), - - ('messages.bg.info', - SettingValue(typ.QssColor(), 'black'), - "Background color of an info message."), - - ('messages.border.info', - SettingValue(typ.QssColor(), '#333333'), - "Border color of an info message."), - - ('prompts.fg', - SettingValue(typ.QssColor(), 'white'), - "Foreground color for prompts."), - - ('prompts.bg', - SettingValue(typ.QssColor(), 'darkblue'), - "Background color for prompts."), - - ('prompts.selected.bg', - SettingValue(typ.QssColor(), '#308cc6'), - "Background color for the selected item in filename prompts."), - - readonly=readonly - )), - - ('fonts', sect.KeyValue( - ('_monospace', - SettingValue(typ.Font(), 'xos4 Terminus, Terminus, Monospace, ' - '"DejaVu Sans Mono", Monaco, ' - '"Bitstream Vera Sans Mono", "Andale Mono", ' - '"Courier New", Courier, "Liberation Mono", ' - 'monospace, Fixed, Consolas, Terminal'), - "Default monospace fonts."), - - ('completion', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used in the completion widget."), - - ('completion.category', - SettingValue(typ.Font(), 'bold ' + DEFAULT_FONT_SIZE + MONOSPACE), - "Font used in the completion categories."), - - ('tabbar', - SettingValue(typ.QtFont(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used in the tab bar."), - - ('statusbar', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used in the statusbar."), - - ('downloads', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used for the downloadbar."), - - ('hints', - SettingValue(typ.Font(), 'bold 13px ${_monospace}'), - "Font used for the hints."), - - ('debug-console', - SettingValue(typ.QtFont(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used for the debugging console."), - - ('web-family-standard', - SettingValue(typ.FontFamily(none_ok=True), ''), - "Font family for standard fonts."), - - ('web-family-fixed', - SettingValue(typ.FontFamily(none_ok=True), ''), - "Font family for fixed fonts."), - - ('web-family-serif', - SettingValue(typ.FontFamily(none_ok=True), ''), - "Font family for serif fonts."), - - ('web-family-sans-serif', - SettingValue(typ.FontFamily(none_ok=True), ''), - "Font family for sans-serif fonts."), - - ('web-family-cursive', - SettingValue(typ.FontFamily(none_ok=True), ''), - "Font family for cursive fonts."), - - ('web-family-fantasy', - SettingValue(typ.FontFamily(none_ok=True), ''), - "Font family for fantasy fonts."), - - # Defaults for web-size-* from WebEngineSettings::initDefaults in - # qtwebengine/src/core/web_engine_settings.cpp and - # QWebSettings::QWebSettings() in - # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp - - ('web-size-minimum', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '0'), - "The hard minimum font size."), - - # This is 0 as default on QtWebKit, and 6 on QtWebEngine - so let's - # just go for 6 here. - ('web-size-minimum-logical', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '6'), - "The minimum logical font size that is applied when zooming " - "out."), - - ('web-size-default', - SettingValue(typ.Int(minval=1, maxval=MAXVALS['int']), '16'), - "The default font size for regular text."), - - ('web-size-default-fixed', - SettingValue(typ.Int(minval=1, maxval=MAXVALS['int']), '13'), - "The default font size for fixed-pitch text."), - - ('keyhint', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used in the keyhint widget."), - - ('messages.error', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used for error messages."), - - ('messages.warning', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used for warning messages."), - - ('messages.info', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + MONOSPACE), - "Font used for info messages."), - - ('prompts', - SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' sans-serif'), - "Font used for prompts."), - - readonly=readonly - )), - ]) - - -DATA = data(readonly=True) - - KEY_FIRST_COMMENT = """ # vim: ft=conf # @@ -1858,3 +522,151 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r'^tab-only -r$'), r'tab-only --next'), (re.compile(r'^tab-only --right$'), r'tab-only --next'), ] + + +Option = collections.namedtuple('Option', ['typ', 'default', 'backends', + 'description']) + + +def _raise_invalid_node(name, what, node): + """Raise an exception for an invalid configdata YAML node. + + Args: + name: The name of the setting being parsed. + what: The name of the thing being parsed. + node: The invalid node. + """ + raise ValueError("Invalid node for {} while reading {}: {!r}".format( + name, what, node)) + + +def _parse_yaml_type(name, node): + if isinstance(node, str): + # e.g: + # type: Bool + # -> create the type object without any arguments + type_name = node + kwargs = {} + elif isinstance(node, dict): + # e.g: + # type: + # name: String + # none_ok: true + # -> create the type object and pass arguments + type_name = node.pop('name') + kwargs = node + else: + _raise_invalid_node(name, 'type', node) + + try: + typ = getattr(configtypes, type_name) + except AttributeError as e: + raise AttributeError("Did not find type {} for {}".format( + type_name, name)) + + # Parse sub-types + try: + if typ is configtypes.Dict: + kwargs['keytype'] = _parse_yaml_type(name, kwargs['keytype']) + kwargs['valtype'] = _parse_yaml_type(name, kwargs['valtype']) + elif typ is configtypes.List: + kwargs['valtype'] = _parse_yaml_type(name, kwargs['valtype']) + except KeyError as e: + _raise_invalid_node(name, str(e), node) + + try: + return typ(**kwargs) + except TypeError as e: + raise TypeError("Error while creating {} with {}: {}".format( + type_name, node, e)) + + +def _parse_yaml_backends_dict(name, node): + """Parse a dict definition for backends. + + Example: + + backends: + QtWebKit: true + QtWebEngine: Qt 5.9 + """ + str_to_backend = { + 'QtWebKit': usertypes.Backend.QtWebKit, + 'QtWebEngine': usertypes.Backend.QtWebEngine, + } + + if node.keys() != str_to_backend.keys(): + _raise_invalid_node(name, 'backends', node) + + backends = [] + + # The value associated to the key, and whether we should add that backend or + # not. + conditionals = { + True: True, + False: False, + 'Qt 5.8': qtutils.version_check('5.8'), + 'Qt 5.9': qtutils.version_check('5.9'), + } + for key in node.keys(): + if conditionals[node[key]]: + backends.append(str_to_backend[key]) + + return backends + + +def _parse_yaml_backends(name, node): + """Parse a backend node in the yaml. + + It can have one of those four forms: + - Not present -> setting applies to both backends. + - backend: QtWebKit -> setting only available with QtWebKit + - backend: QtWebEngine -> setting only available with QtWebEngine + - backend: + QtWebKit: true + QtWebEngine: Qt 5.9 + -> setting available based on the given conditionals. + """ + if node is None: + return [usertypes.Backend.QtWebKit, usertypes.Backend.QtWebEngine] + elif node == 'QtWebKit': + return [usertypes.Backend.QtWebKit] + elif node == 'QtWebEngine': + return [usertypes.Backend.QtWebEngine] + elif isinstance(node, dict): + return _parse_yaml_backends_dict(name, node) + _raise_invalid_node(name, 'backends', node) + + +def _read_yaml(yaml_data): + """Read config data from a YAML file. + + Args: + yaml_data: The YAML string to parse. + + Return: + A dict mapping option names to Option elements. + """ + parsed = {} + data = yaml.load(yaml_data) + + keys = {'type', 'default', 'desc', 'backend'} + + for name, option in data.items(): + if not set(option.keys()).issubset(keys): + raise ValueError("Invalid keys {} for {}".format( + option.keys(), name)) + + parsed[name] = Option( + typ=_parse_yaml_type(name, option['type']), + default=option['default'], + backends=_parse_yaml_backends(name, option.get('backend', None)), + description=option['desc']) + + return parsed + + +def init(): + """Initialize configdata from the YAML file.""" + global DATA + DATA = _read_yaml(utils.read_file('config/configdata.yml')) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 5245095f6..a66875dba 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -8,14 +8,14 @@ ignore_case: start_page: type: name: List - elemtype: String + valtype: String default: ["https://start.duckduckgo.com"] desc: The default page(s) to open at the start. yank_ignored_url_parameters: type: name: List - elemtype: String + valtype: String default: - ref - utm_source @@ -78,7 +78,7 @@ editor.command: editor.encoding: type: Encoding default: utf-8 - desc: Encoding to use for the editor. + desc : Encoding to use for the editor. content.private_browsing: type: Bool @@ -198,7 +198,7 @@ history_session_interval: zoom.levels: type: name: List - elemtype: + valtype: name: Perc minval: 0 default: @@ -354,7 +354,7 @@ window.hide_wayland_decoration: keyhint.blacklist: type: name: List - elemtype: + valtype: name: String none_ok: true default: "" @@ -417,11 +417,7 @@ content.user_agent: content.proxy: default: system - type: - name: Proxy - valid_values: - - system: "Use the system wide proxy." - - none: "Don't use any proxy" + type: Proxy backend: QtWebKit: true QtWebEngine: Qt 5.8 @@ -1015,7 +1011,7 @@ content.host_blocking.lists: - "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext" type: name: List - elemtype: Url + valtype: Url none_ok: true desc: | List of URLs of lists which contain hosts to block. @@ -1037,7 +1033,7 @@ content.host_blocking.whitelist: - piwik.org type: name: List - valtype: string + valtype: String none_ok: true desc: >- List of domains that should always be loaded, despite being ad-blocked. diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 3d6e71426..6f11ab1ef 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar from qutebrowser.commands import cmdutils from qutebrowser.config import configexc -from qutebrowser.utils import standarddir, utils +from qutebrowser.utils import standarddir, utils, qtutils SYSTEM_PROXY = object() # Return value for Proxy type @@ -313,27 +313,27 @@ class List(BaseType): """Base class for a (string-)list setting.""" - _show_inner_type = True + _show_valtype = True - def __init__(self, inner_type, none_ok=False, length=None): + def __init__(self, valtype, none_ok=False, length=None): super().__init__(none_ok) - self.inner_type = inner_type + self.valtype = valtype self.length = length def get_name(self): name = super().get_name() - if self._show_inner_type: - name += " of " + self.inner_type.get_name() + if self._show_valtype: + name += " of " + self.valtype.get_name() return name def get_valid_values(self): - return self.inner_type.get_valid_values() + return self.valtype.get_valid_values() def transform(self, value): if not value: return None else: - return [self.inner_type.transform(v.strip()) + return [self.valtype.transform(v.strip()) for v in value.split(',')] def validate(self, value): @@ -345,7 +345,7 @@ class List(BaseType): raise configexc.ValidationError(value, "Exactly {} values need to " "be set!".format(self.length)) for val in vals: - self.inner_type.validate(val.strip()) + self.valtype.validate(val.strip()) class FlagList(List): @@ -358,14 +358,14 @@ class FlagList(List): combinable_values = None - _show_inner_type = False + _show_valtype = False def __init__(self, none_ok=False, valid_values=None): super().__init__(BaseType(), none_ok) - self.inner_type.valid_values = valid_values + self.valtype.valid_values = valid_values def validate(self, value): - if self.inner_type.valid_values is not None: + if self.valtype.valid_values is not None: super().validate(value) else: self._basic_validation(value) @@ -379,7 +379,7 @@ class FlagList(List): value, "List contains duplicate values!") def complete(self): - valid_values = self.inner_type.valid_values + valid_values = self.valtype.valid_values if valid_values is None: return None @@ -453,11 +453,22 @@ class Int(BaseType): def __init__(self, minval=None, maxval=None, none_ok=False): super().__init__(none_ok) - if maxval is not None and minval is not None and maxval < minval: - raise ValueError("minval ({}) needs to be <= maxval ({})!".format( - minval, maxval)) - self.minval = minval - self.maxval = maxval + self.minval = self._parse_limit(minval) + self.maxval = self._parse_limit(maxval) + if self.maxval is not None and self.minval is not None: + if self.maxval < self.minval: + raise ValueError("minval ({}) needs to be <= maxval ({})!" + .format(self.minval, self.maxval)) + + def _parse_limit(self, value): + if value == 'maxint': + return qtutils.MAXVALS['int'] + elif value == 'maxint64': + return qtutils.MAXVALS['int64'] + else: + if value is not None: + assert isinstance(value, int), value + return value def transform(self, value): if not value: @@ -1056,12 +1067,12 @@ class Padding(List): """Setting for paddings around elements.""" - _show_inner_type = False + _show_valtype = False def __init__(self, none_ok=False, valid_values=None): super().__init__(Int(minval=0, none_ok=none_ok), none_ok=none_ok, length=4) - self.inner_type.valid_values = valid_values + self.valtype.valid_values = valid_values def transform(self, value): elems = super().transform(value) @@ -1179,10 +1190,17 @@ class Url(BaseType): "{}".format(val.errorString())) -class HeaderDict(BaseType): +class Dict(BaseType): """A JSON-like dictionary for custom HTTP headers.""" + # FIXME:conf validate correctly + + def __init__(self, keytype, valtype, none_ok=False): + super().__init__(none_ok) + self.keytype = keytype + self.valtype = valtype + def _validate_str(self, value, what): """Check if the given thing is an ascii-only string. @@ -1274,8 +1292,8 @@ class ConfirmQuit(FlagList): def __init__(self, none_ok=False): super().__init__(none_ok) - self.inner_type.none_ok = none_ok - self.inner_type.valid_values = ValidValues( + self.valtype.none_ok = none_ok + self.valtype.valid_values = ValidValues( ('always', "Always show a confirmation."), ('multiple-tabs', "Show a confirmation if " "multiple tabs are opened."), diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index f4c350d66..807ad0bfe 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -48,14 +48,16 @@ class NewConfigManager(QObject): super().__init__(parent) self._values = {} - def _key(self, sect, opt): - return sect + ' -> ' + opt + def _key(self, sect, opt=None): + if opt is None: + # New usage + return sect + return sect + '.' + opt def read_defaults(self): - for name, section in configdata.data().items(): - for key, value in section.items(): - self._values[self._key(name, key)] = value + for name, option in configdata.DATA.items(): + self._values[name] = option def get(self, section, option): val = self._values[self._key(section, option)] - return val.typ.transform(val.value()) + return val.typ.transform(val.default) diff --git a/scripts/dev/pylint_checkers/qute_pylint/config.py b/scripts/dev/pylint_checkers/qute_pylint/config.py index be8ac8da8..f95979bbd 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/config.py +++ b/scripts/dev/pylint_checkers/qute_pylint/config.py @@ -23,16 +23,11 @@ import sys import os import os.path +import yaml import astroid from pylint import interfaces, checkers from pylint.checkers import utils -sys.path.insert( - 0, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, - os.pardir)) - -from qutebrowser.config import configdata - class ConfigChecker(checkers.BaseChecker): diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py index fdb7f00e4..e300318d4 100644 --- a/tests/unit/config/test_configdata.py +++ b/tests/unit/config/test_configdata.py @@ -18,27 +18,198 @@ """Tests for qutebrowser.config.configdata.""" +import textwrap + +import yaml import pytest -from qutebrowser.config import configdata +from qutebrowser.config import configdata, configtypes +from qutebrowser.utils import usertypes -@pytest.mark.parametrize('sect', configdata.DATA.keys()) -def test_section_desc(sect): - """Make sure every section has a description.""" - desc = configdata.SECTION_DESC[sect] - assert isinstance(desc, str) +def test_init(): + """Test reading the default yaml file.""" + configdata.init() + assert isinstance(configdata.DATA, dict) + assert 'ignore_case' in configdata.DATA -def test_data(): - """Some simple sanity tests on data().""" - data = configdata.data() - assert 'general' in data - assert 'ignore-case' in data['general'] +class TestReadYaml: -def test_readonly_data(): - """Make sure DATA is readonly.""" - with pytest.raises(ValueError, match="Trying to modify a read-only " - "config!"): - configdata.DATA['general'].setv('temp', 'ignore-case', 'true', 'true') + def test_valid(self): + data = textwrap.dedent(""" + test1: + type: Bool + default: true + desc: Hello World + + test2: + type: String + default: foo + backend: QtWebKit + desc: Hello World 2 + """) + data = configdata._read_yaml(data) + assert data.keys() == {'test1', 'test2'} + assert data['test1'].description == "Hello World" + assert data['test2'].default == "foo" + assert data['test2'].backends == [usertypes.Backend.QtWebKit] + assert isinstance(data['test1'].typ, configtypes.Bool) + + def test_invalid_keys(self): + """Test reading with unknown keys.""" + data = textwrap.dedent(""" + test: + type: Bool + default: true + desc: Hello World + hello: world + """,) + with pytest.raises(ValueError, match='Invalid keys'): + configdata._read_yaml(data) + + +class TestParseYamlType: + + def _yaml(self, s): + """Get the type from parsed YAML data.""" + return yaml.load(textwrap.dedent(s))['type'] + + def test_simple(self): + """Test type which is only a name.""" + data = self._yaml("type: Bool") + typ = configdata._parse_yaml_type('test', data) + assert isinstance(typ, configtypes.Bool) + assert not typ.none_ok + + def test_complex(self): + """Test type parsing with arguments.""" + data = self._yaml(""" + type: + name: String + minlen: 2 + """) + typ = configdata._parse_yaml_type('test', data) + assert isinstance(typ, configtypes.String) + assert not typ.none_ok + assert typ.minlen == 2 + + def test_list(self): + """Test type parsing with a list and subtypes.""" + data = self._yaml(""" + type: + name: List + valtype: String + """) + typ = configdata._parse_yaml_type('test', data) + assert isinstance(typ, configtypes.List) + assert isinstance(typ.valtype, configtypes.String) + assert not typ.none_ok + assert not typ.valtype.none_ok + + def test_dict(self): + """Test type parsing with a dict and subtypes.""" + data = self._yaml(""" + type: + name: Dict + keytype: String + valtype: + name: Int + minval: 10 + """) + typ = configdata._parse_yaml_type('test', data) + assert isinstance(typ, configtypes.Dict) + assert isinstance(typ.keytype, configtypes.String) + assert isinstance(typ.valtype, configtypes.Int) + assert not typ.none_ok + assert typ.valtype.minval == 10 + + def test_invalid_node(self): + """Test type parsing with invalid node type.""" + data = self._yaml("type: 42") + with pytest.raises(ValueError, match="Invalid node for test while " + "reading type: 42"): + configdata._parse_yaml_type('test', data) + + def test_unknown_type(self): + """Test type parsing with type which doesn't exist.""" + data = self._yaml("type: Foobar") + with pytest.raises(AttributeError, + match="Did not find type Foobar for test"): + configdata._parse_yaml_type('test', data) + + def test_unknown_dict(self): + """Test type parsing with a dict without keytype.""" + data = self._yaml("type: Dict") + with pytest.raises(ValueError, match="Invalid node for test while " + "reading 'keytype': 'Dict'"): + configdata._parse_yaml_type('test', data) + + def test_unknown_args(self): + """Test type parsing with unknown type arguments.""" + data = self._yaml(""" + type: + name: Int + answer: 42 + """) + with pytest.raises(TypeError, match="Error while creating Int"): + configdata._parse_yaml_type('test', data) + + +class TestParseYamlBackend: + + def _yaml(self, s): + """Get the type from parsed YAML data.""" + return yaml.load(textwrap.dedent(s))['backend'] + + @pytest.mark.parametrize('backend, expected', [ + ('QtWebKit', [usertypes.Backend.QtWebKit]), + ('QtWebEngine', [usertypes.Backend.QtWebEngine]), + # This is also what _parse_yaml_backends gets when backend: is not given + # at all + ('null', [usertypes.Backend.QtWebKit, usertypes.Backend.QtWebEngine]), + ]) + def test_simple(self, backend, expected): + """Check a simple "backend: QtWebKit".""" + data = self._yaml("backend: {}".format(backend)) + backends = configdata._parse_yaml_backends('test', data) + assert backends == expected + + @pytest.mark.parametrize('webkit, has_new_version, expected', [ + (True, True, [usertypes.Backend.QtWebKit, + usertypes.Backend.QtWebEngine]), + (False, True, [usertypes.Backend.QtWebEngine]), + (True, False, [usertypes.Backend.QtWebKit]), + ]) + def test_dict(self, monkeypatch, webkit, has_new_version, expected): + data = self._yaml(""" + backend: + QtWebKit: {} + QtWebEngine: Qt 5.8 + """.format('true' if webkit else 'false')) + monkeypatch.setattr(configdata.qtutils, 'version_check', + lambda v: has_new_version) + backends = configdata._parse_yaml_backends('test', data) + assert backends == expected + + @pytest.mark.parametrize('yaml_data', [ + # Wrong type + "backend: 42", + # Unknown key + """ + backend: + QtWebKit: true + QtWebEngine: true + foo: bar + """, + # Missing key + """ + backend: + QtWebKit: true + """, + ]) + def test_invalid_backend(self, yaml_data): + with pytest.raises(ValueError, match="Invalid node for test while " + "reading backends:"): + configdata._parse_yaml_backends('test', self._yaml(yaml_data)) From c856f6d97b409adfda5af07579b5f8aaa06eea8a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 9 Jun 2017 19:54:25 +0200 Subject: [PATCH 005/516] Initial work on new pylint checker --- .../dev/pylint_checkers/qute_pylint/config.py | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/scripts/dev/pylint_checkers/qute_pylint/config.py b/scripts/dev/pylint_checkers/qute_pylint/config.py index f95979bbd..60334f3fd 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/config.py +++ b/scripts/dev/pylint_checkers/qute_pylint/config.py @@ -36,9 +36,12 @@ class ConfigChecker(checkers.BaseChecker): __implements__ = interfaces.IAstroidChecker name = 'config' msgs = { - 'E0000': ('"%s -> %s" is no valid config option.', # flake8: disable=S001 + 'E0000': ('%s is no valid config option.', # flake8: disable=S001 'bad-config-call', None), + 'E0001': ('old config call', # flake8: disable=S001 + 'old-config-call', + None), } priority = -1 @@ -53,23 +56,25 @@ class ConfigChecker(checkers.BaseChecker): def _check_config(self, node): """Check that the arguments to config.get(...) are valid.""" - try: - sect_arg = utils.get_argument_from_call(node, position=0, - keyword='sectname') - opt_arg = utils.get_argument_from_call(node, position=1, - keyword='optname') - except utils.NoSuchArgumentError: - return - sect_arg = utils.safe_infer(sect_arg) - opt_arg = utils.safe_infer(opt_arg) - if not (isinstance(sect_arg, astroid.Const) and - isinstance(opt_arg, astroid.Const)): - return - try: - configdata.DATA[sect_arg.value][opt_arg.value] - except KeyError: - self.add_message('bad-config-call', node=node, - args=(sect_arg.value, opt_arg.value)) + # try: + # sect_arg = utils.get_argument_from_call(node, position=0, + # keyword='sectname') + # opt_arg = utils.get_argument_from_call(node, position=1, + # keyword='optname') + # except utils.NoSuchArgumentError: + # return + # sect_arg = utils.safe_infer(sect_arg) + # opt_arg = utils.safe_infer(opt_arg) + # if not (isinstance(sect_arg, astroid.Const) and + # isinstance(opt_arg, astroid.Const)): + # return + # try: + # configdata.DATA[sect_arg.value][opt_arg.value] + # except KeyError: + # self.add_message('bad-config-call', node=node, + # args=(sect_arg.value, opt_arg.value)) + + self.add_message('old-config-call', node=node) def register(linter): From 8b9b750f8f455c32aaf1f32ccfc836dc3c1e009b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 12 Jun 2017 11:27:19 +0200 Subject: [PATCH 006/516] configdata: Rename some options for consistency --- qutebrowser/config/configdata.yml | 88 +++++++++++++++---------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index a66875dba..46f3f20a2 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1266,72 +1266,72 @@ colors.completion.scrollbar.bg: type: QssColor desc: Color of the scrollbar in completion view -colors.statusbar.fg.normal: +colors.statusbar.normal.fg: default: white type: QssColor desc: Foreground color of the statusbar. -colors.statusbar.bg.normal: +colors.statusbar.normal.bg: default: black type: QssColor desc: Background color of the statusbar. -colors.statusbar.fg.private: +colors.statusbar.private.fg: default: white type: QssColor desc: Foreground color of the statusbar in private browsing mode. -colors.statusbar.bg.private: +colors.statusbar.private.bg: default: '#666666' type: QssColor desc: Background color of the statusbar in private browsing mode. -colors.statusbar.fg.insert: +colors.statusbar.insert.fg: default: white type: QssColor desc: Foreground color of the statusbar in insert mode. -colors.statusbar.bg.insert: +colors.statusbar.insert.bg: default: darkgreen type: QssColor desc: Background color of the statusbar in insert mode. -colors.statusbar.fg.command: +colors.statusbar.command.fg: default: white type: QssColor desc: Foreground color of the statusbar in command mode. -colors.statusbar.bg.command: +colors.statusbar.command.bg: default: black type: QssColor desc: Background color of the statusbar in command mode. -colors.statusbar.fg.command_private: +colors.statusbar.command.private.fg: default: white type: QssColor desc: Foreground color of the statusbar in private browsing + command mode. -colors.statusbar.bg.command_private: +colors.statusbar.command.private.bg: default: grey type: QssColor desc: Background color of the statusbar in private browsing + command mode. -colors.statusbar.fg.caret: +colors.statusbar.caret.fg: default: white type: QssColor desc: Foreground color of the statusbar in caret mode. -colors.statusbar.bg.caret: +colors.statusbar.caret.bg: default: purple type: QssColor desc: Background color of the statusbar in caret mode. -colors.statusbar.fg.caret_selection: +colors.statusbar.caret.selection.fg: default: white type: QssColor desc: Foreground color of the statusbar in caret mode with a selection -colors.statusbar.bg.caret_selection: +colors.statusbar.caret.selection.bg: default: '#a12dff' type: QssColor desc: Background color of the statusbar in caret mode with a selection @@ -1356,62 +1356,62 @@ colors.statusbar.url.success.https.bg: type: QssColor desc: Foreground color of the URL in the statusbar on successful load (https). -colors.statusbar.url.fg.error: +colors.statusbar.url.error.fg: default: orange type: QssColor desc: Foreground color of the URL in the statusbar on error. -colors.statusbar.url.fg.warn: +colors.statusbar.url.warn.fg: default: yellow type: QssColor desc: "Foreground color of the URL in the statusbar when there's a warning." -colors.statusbar.url.fg.hover: +colors.statusbar.url.hover.fg: default: aqua desc: Foreground color of the URL in the statusbar for hovered links. type: QssColor -colors.tabs.fg.odd: +colors.tabs.odd.fg: default: white type: QtColor desc: Foreground color of unselected odd tabs. -colors.tabs.bg.odd: +colors.tabs.odd.bg: default: grey type: QtColor desc: Background color of unselected odd tabs. -colors.tabs.fg.even: +colors.tabs.even.fg: default: white type: QtColor desc: Foreground color of unselected even tabs. -colors.tabs.bg.even: +colors.tabs.even.bg: default: darkgrey type: QtColor desc: Background color of unselected even tabs. -colors.tabs.fg.selected.odd: +colors.tabs.selected.odd.fg: default: white type: QtColor desc: Foreground color of selected odd tabs. -colors.tabs.bg.selected.odd: +colors.tabs.selected.odd.bg: default: black type: QtColor desc: Background color of selected odd tabs. -colors.tabs.fg.selected.even: +colors.tabs.selected.even.fg: default: white type: QtColor desc: Foreground color of selected even tabs. -colors.tabs.bg.selected.even: +colors.tabs.selected.even.bg: default: black type: QtColor desc: Background color of selected even tabs. -colors.tabs.bg.bar: +colors.tabs.bar.bg: default: '#555555' type: QtColor desc: Background color of the tab bar. @@ -1453,47 +1453,47 @@ colors.hints.match.fg: type: QssColor desc: Font color for the matched part of hints. -colors.downloads.bg.bar: +colors.downloads.bar.bg: default: black type: QssColor desc: Background color for the download bar. -colors.downloads.fg.start: +colors.downloads.start.fg: default: white type: QtColor desc: Color gradient start for download text. -colors.downloads.bg.start: +colors.downloads.start.bg: default: '#0000aa' type: QtColor desc: Color gradient start for download backgrounds. -colors.downloads.fg.stop: +colors.downloads.stop.fg: default: '#0000aa' type: QtColor desc: Color gradient end for download text. -colors.downloads.bg.stop: +colors.downloads.stop.bg: default: '#00aa00' type: QtColor desc: Color gradient stop for download backgrounds. -colors.downloads.fg.system: +colors.downloads.system.fg: default: rgb type: ColorSystem desc: Color gradient interpolation system for download text. -colors.downloads.bg.system: +colors.downloads.system.bg: default: rgb type: ColorSystem desc: Color gradient interpolation system for download backgrounds. -colors.downloads.fg.error: +colors.downloads.error.fg: default: white type: QtColor desc: Foreground color for downloads with errors. -colors.downloads.bg.error: +colors.downloads.error.bg: default: red type: QtColor desc: Background color for downloads with errors. @@ -1520,47 +1520,47 @@ colors.keyhint.bg: type: QssColor desc: Background color of the keyhint widget. -colors.messages.fg.error: +colors.messages.error.fg: default: white type: QssColor desc: Foreground color of an error message. -colors.messages.bg.error: +colors.messages.error.bg: default: red type: QssColor desc: Background color of an error message. -colors.messages.border.error: +colors.messages.error.border: default: '#bb0000' type: QssColor desc: Border color of an error message. -colors.messages.fg.warning: +colors.messages.warning.fg: default: white type: QssColor desc: Foreground color a warning message. -colors.messages.bg.warning: +colors.messages.warning.bg: default: darkorange type: QssColor desc: Background color of a warning message. -colors.messages.border.warning: +colors.messages.warning.border: default: '#d47300' type: QssColor desc: Border color of an error message. -colors.messages.fg.info: +colors.messages.info.fg: default: white type: QssColor desc: Foreground color an info message. -colors.messages.bg.info: +colors.messages.info.bg: default: black type: QssColor desc: Background color of an info message. -colors.messages.border.info: +colors.messages.info.border: default: '#333333' type: QssColor desc: Border color of an info message. From 938946c48bf16a676baa611c7668f1b3c4487b93 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 12 Jun 2017 11:40:10 +0200 Subject: [PATCH 007/516] configdata: Add check for shadowing keys --- qutebrowser/config/configdata.py | 6 ++++++ qutebrowser/config/configdata.yml | 2 +- tests/unit/config/test_configdata.py | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index db1a9ce34..510769dbb 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -663,6 +663,12 @@ def _read_yaml(yaml_data): backends=_parse_yaml_backends(name, option.get('backend', None)), description=option['desc']) + # Make sure no key shadows another. + for key1 in parsed: + for key2 in parsed: + if key2.startswith(key1 + '.'): + raise ValueError("Shadowing keys {} and {}".format(key1, key2)) + return parsed diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 46f3f20a2..69298b934 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -904,7 +904,7 @@ content.images: type: Bool desc: Whether images are automatically loaded in web pages. -content.javascript: +content.javascript.enabled: default: true type: Bool desc: Enables or disables the running of JavaScript programs. diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py index e300318d4..81b680f08 100644 --- a/tests/unit/config/test_configdata.py +++ b/tests/unit/config/test_configdata.py @@ -69,6 +69,31 @@ class TestReadYaml: with pytest.raises(ValueError, match='Invalid keys'): configdata._read_yaml(data) + @pytest.mark.parametrize('first, second, shadowing', [ + ('foo', 'foo.bar', True), + ('foo.bar', 'foo', True), + ('foo.bar', 'foo.bar.baz', True), + ('foo.bar', 'foo.baz', False), + ]) + def test_shadowing(self, first, second, shadowing): + """Make sure a setting can't shadow another.""" + data = textwrap.dedent(""" + {first}: + type: Bool + default: true + desc: Hello World + + {second}: + type: Bool + default: true + desc: Hello World + """.format(first=first, second=second)) + if shadowing: + with pytest.raises(ValueError, match='Shadowing keys'): + configdata._read_yaml(data) + else: + configdata._read_yaml(data) + class TestParseYamlType: From c2e75bf2fdbc06336668dcb0211e9e42d3091542 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 12 Jun 2017 13:19:56 +0200 Subject: [PATCH 008/516] Initial conversion to new config syntax --- qutebrowser/app.py | 8 +-- qutebrowser/browser/adblock.py | 12 ++-- qutebrowser/browser/browsertab.py | 10 +-- qutebrowser/browser/commands.py | 22 +++--- qutebrowser/browser/downloads.py | 8 +-- qutebrowser/browser/hints.py | 26 +++---- qutebrowser/browser/mouse.py | 10 +-- qutebrowser/browser/navigate.py | 2 +- qutebrowser/browser/network/proxy.py | 6 +- qutebrowser/browser/qtnetworkdownloads.py | 2 +- qutebrowser/browser/qutescheme.py | 4 +- qutebrowser/browser/shared.py | 22 +++--- qutebrowser/browser/webelem.py | 6 +- qutebrowser/browser/webengine/interceptor.py | 2 +- .../browser/webengine/webenginesettings.py | 4 +- qutebrowser/browser/webengine/webview.py | 6 +- qutebrowser/browser/webkit/cache.py | 2 +- qutebrowser/browser/webkit/cookies.py | 6 +- .../browser/webkit/network/networkmanager.py | 4 +- qutebrowser/browser/webkit/webkitelem.py | 2 +- qutebrowser/browser/webkit/webkitinspector.py | 2 +- qutebrowser/browser/webkit/webkitsettings.py | 2 +- qutebrowser/browser/webkit/webpage.py | 4 +- qutebrowser/browser/webkit/webview.py | 4 +- qutebrowser/commands/userscripts.py | 2 +- qutebrowser/completion/completer.py | 2 +- qutebrowser/completion/completiondelegate.py | 4 +- qutebrowser/completion/completionwidget.py | 16 ++--- qutebrowser/completion/models/urlmodel.py | 4 +- qutebrowser/config/config.py | 10 ++- qutebrowser/config/newconfig.py | 70 ++++++++++++++++--- qutebrowser/keyinput/basekeyparser.py | 2 +- qutebrowser/keyinput/modeparsers.py | 2 +- qutebrowser/mainwindow/mainwindow.py | 14 ++-- qutebrowser/mainwindow/messageview.py | 4 +- qutebrowser/mainwindow/prompt.py | 8 +-- qutebrowser/mainwindow/statusbar/bar.py | 6 +- qutebrowser/mainwindow/tabbedbrowser.py | 48 ++++++------- qutebrowser/mainwindow/tabwidget.py | 48 ++++++------- qutebrowser/misc/consolewidget.py | 4 +- qutebrowser/misc/crashdialog.py | 4 +- qutebrowser/misc/editor.py | 6 +- qutebrowser/misc/keyhintwidget.py | 8 +-- qutebrowser/misc/savemanager.py | 12 ++-- qutebrowser/misc/sessions.py | 2 +- qutebrowser/utils/urlutils.py | 6 +- qutebrowser/utils/utils.py | 2 +- 47 files changed, 259 insertions(+), 201 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 59aff9476..bdffc2921 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -274,7 +274,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None): if via_ipc and target_arg and target_arg != 'auto': open_target = target_arg else: - open_target = config.get('general', 'new-instance-open-target') + open_target = config.val.new_instance_open_target win_id = mainwindow.get_window(via_ipc, force_target=open_target) tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) @@ -310,7 +310,7 @@ def _open_startpage(win_id=None): window=cur_win_id) if tabbed_browser.count() == 0: log.init.debug("Opening startpage") - for urlstr in config.get('general', 'startpage'): + for urlstr in config.val.startpage: try: url = urlutils.fuzzy_url(urlstr, do_search=False) except urlutils.InvalidUrlError as e: @@ -464,7 +464,7 @@ def _init_modules(args, crash_handler): completionmodels.init() log.init.debug("Misc initialization...") - if config.get('ui', 'hide-wayland-decoration'): + if config.val.ui.hide_wayland_decoration: os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' else: os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None) @@ -674,7 +674,7 @@ class Quitter: if session is not None: session_manager.save(session, last_window=last_window, load_next_time=True) - elif config.get('general', 'save-session'): + elif config.val.save_session: session_manager.save(sessions.default, last_window=last_window, load_next_time=True) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 276dec08b..b27a54424 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -67,7 +67,7 @@ def is_whitelisted_host(host): Args: host: The host of the request as string. """ - whitelist = config.get('content', 'host-blocking-whitelist') + whitelist = config.val.content.host_blocking_whitelist if whitelist is None: return False @@ -123,7 +123,7 @@ class HostBlocker: def is_blocked(self, url): """Check if the given URL (as QUrl) is blocked.""" - if not config.get('content', 'host-blocking-enabled'): + if not config.val.content.host_blocking_enabled: return False host = url.host() return ((host in self._blocked_hosts or @@ -164,9 +164,9 @@ class HostBlocker: if not found: args = objreg.get('args') - if (config.get('content', 'host-block-lists') is not None and + if (config.val.content.host_block_lists is not None and args.basedir is None and - config.get('content', 'host-blocking-enabled')): + config.val.content.host_blocking_enabled): message.info("Run :adblock-update to get adblock lists.") @cmdutils.register(instance='host-blocker') @@ -180,7 +180,7 @@ class HostBlocker: self._config_blocked_hosts) self._blocked_hosts = set() self._done_count = 0 - urls = config.get('content', 'host-block-lists') + urls = config.val.content.host_block_lists download_manager = objreg.get('qtnetwork-download-manager', scope='window', window='last-focused') if urls is None: @@ -295,7 +295,7 @@ class HostBlocker: @config.change_filter('content', 'host-block-lists') def on_config_changed(self): """Update files when the config changed.""" - urls = config.get('content', 'host-block-lists') + urls = config.val.content.host_block_lists if urls is None: try: os.remove(self._local_hosts_file) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index b36b5d1c3..7adb9608e 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -249,17 +249,17 @@ class AbstractZoom(QObject): def _on_config_changed(self, section, option): if section == 'ui' and option in ['zoom-levels', 'default-zoom']: if not self._default_zoom_changed: - factor = float(config.get('ui', 'default-zoom')) / 100 + factor = float(config.val.ui.default_zoom) / 100 self._set_factor_internal(factor) self._default_zoom_changed = False self._init_neighborlist() def _init_neighborlist(self): """Initialize self._neighborlist.""" - levels = config.get('ui', 'zoom-levels') + levels = config.val.ui.zoom_levels self._neighborlist = usertypes.NeighborList( levels, mode=usertypes.NeighborList.Modes.edge) - self._neighborlist.fuzzyval = config.get('ui', 'default-zoom') + self._neighborlist.fuzzyval = config.val.ui.default_zoom def offset(self, offset): """Increase/Decrease the zoom level by the given offset. @@ -295,7 +295,7 @@ class AbstractZoom(QObject): raise NotImplementedError def set_default(self): - default_zoom = config.get('ui', 'default-zoom') + default_zoom = config.val.ui.default_zoom self._set_factor_internal(float(default_zoom) / 100) @@ -690,7 +690,7 @@ class AbstractTab(QWidget): def _handle_auto_insert_mode(self, ok): """Handle auto-insert-mode after loading finished.""" - if not config.get('input', 'auto-insert-mode') or not ok: + if not config.val.input.auto_insert_mode or not ok: return cur_mode = self._mode_manager.mode diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3d8516fe2..c375188f4 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -190,7 +190,7 @@ class CommandDispatcher: elif next_: return QTabBar.SelectRightTab elif opposite: - conf_selection = config.get('tabs', 'select-on-remove') + conf_selection = config.val.tabs.select_on_remove if conf_selection == QTabBar.SelectLeftTab: return QTabBar.SelectRightTab elif conf_selection == QTabBar.SelectRightTab: @@ -307,7 +307,7 @@ class CommandDispatcher: private: Open a new window in private browsing mode. """ if url is None: - urls = [config.get('general', 'default-page')] + urls = [config.val.default_page] else: urls = self._parse_url_input(url) @@ -507,9 +507,9 @@ class CommandDispatcher: idx = new_tabbed_browser.indexOf(newtab) new_tabbed_browser.set_page_title(idx, cur_title) - if config.get('tabs', 'show-favicons'): + if config.val.tabs.show_favicons: new_tabbed_browser.setTabIcon(idx, curtab.icon()) - if config.get('tabs', 'tabs-are-windows'): + if config.val.tabs.tabs_are_windows: new_tabbed_browser.window().setWindowIcon(curtab.icon()) newtab.data.keep_icon = True @@ -778,7 +778,7 @@ class CommandDispatcher: url_query.setQueryDelimiters('=', ';') url_query.setQuery(url_query_str) for key in dict(url_query.queryItems()): - if key in config.get('general', 'yank-ignored-url-parameters'): + if key in config.val.yank_ignored_url_parameters: url_query.removeQueryItem(key) url.setQuery(url_query) return url.toString(flags) @@ -888,7 +888,7 @@ class CommandDispatcher: level = count if count is not None else zoom if level is None: - level = config.get('ui', 'default-zoom') + level = config.val.ui.default_zoom tab = self._current_widget() try: @@ -953,7 +953,7 @@ class CommandDispatcher: newidx = self._current_index() - count if newidx >= 0: self._set_current_index(newidx) - elif config.get('tabs', 'wrap'): + elif config.val.tabs.wrap: self._set_current_index(newidx % self._count()) else: raise cmdexc.CommandError("First tab") @@ -973,7 +973,7 @@ class CommandDispatcher: newidx = self._current_index() + count if newidx < self._count(): self._set_current_index(newidx) - elif config.get('tabs', 'wrap'): + elif config.val.tabs.wrap: self._set_current_index(newidx % self._count()) else: raise cmdexc.CommandError("Last tab") @@ -1131,7 +1131,7 @@ class CommandDispatcher: elif index == '+': # pragma: no branch new_idx += delta - if config.get('tabs', 'wrap'): + if config.val.tabs.wrap: new_idx %= self._count() else: # absolute moving @@ -1192,7 +1192,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') def home(self): """Open main startpage in current tab.""" - self.openurl(config.get('general', 'startpage')[0]) + self.openurl(config.val.startpage[0]) def _run_userscript(self, cmd, *args, verbose=False): """Run a userscript given as argument. @@ -1746,7 +1746,7 @@ class CommandDispatcher: return options = { - 'ignore_case': config.get('general', 'ignore-case'), + 'ignore_case': config.val.ignore_case, 'reverse': reverse, } diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index c7cb5ad8e..b70d5ca45 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -69,8 +69,8 @@ class UnsupportedOperationError(Exception): def download_dir(): """Get the download directory to use.""" - directory = config.get('storage', 'download-directory') - remember_dir = config.get('storage', 'remember-download-directory') + directory = config.val.storage.download_directory + remember_dir = config.val.storage.remember_download_directory if remember_dir and last_used_directory is not None: return last_used_directory @@ -104,7 +104,7 @@ def _path_suggestion(filename): Args: filename: The filename to use if included in the suggestion. """ - suggestion = config.get('completion', 'download-path-suggestion') + suggestion = config.val.completion.download_path_suggestion if suggestion == 'path': # add trailing '/' if not present return os.path.join(download_dir(), '') @@ -735,7 +735,7 @@ class AbstractDownloadManager(QObject): download.remove_requested.connect(functools.partial( self._remove_item, download)) - delay = config.get('ui', 'remove-finished-downloads') + delay = config.val.ui.remove_finished_downloads if delay > -1: download.finished.connect( lambda: QTimer.singleShot(delay, download.remove)) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 3cc70f434..aed554d9f 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -68,7 +68,7 @@ class HintLabel(QLabel): background-color: {{ color['hints.bg'] }}; color: {{ color['hints.fg'] }}; font: {{ font['hints'] }}; - border: {{ config.get('hints', 'border') }}; + border: {{ config.val.hints.border }}; padding-left: -3px; padding-right: -3px; } @@ -100,7 +100,7 @@ class HintLabel(QLabel): matched: The part of the text which was typed. unmatched: The part of the text which was not typed yet. """ - if (config.get('hints', 'uppercase') and + if (config.val.hints.uppercase and self._context.hint_mode in ['letter', 'word']): matched = html.escape(matched.upper()) unmatched = html.escape(unmatched.upper()) @@ -108,7 +108,7 @@ class HintLabel(QLabel): matched = html.escape(matched) unmatched = html.escape(unmatched) - match_color = html.escape(config.get('colors', 'hints.fg.match')) + match_color = html.escape(config.val.colors.hints.fg.match) self.setText('{}{}'.format( match_color, matched, unmatched)) self.adjustSize() @@ -121,7 +121,7 @@ class HintLabel(QLabel): log.hints.debug("Frame for {!r} vanished!".format(self)) self.hide() return - no_js = config.get('hints', 'find-implementation') != 'javascript' + no_js = config.val.hints.find_implementation != 'javascript' rect = self.elem.rect_on_view(no_js=no_js) self.move(rect.x(), rect.y()) @@ -203,7 +203,7 @@ class HintActions: Target.window: usertypes.ClickTarget.window, Target.hover: usertypes.ClickTarget.normal, } - if config.get('tabs', 'background-tabs'): + if config.val.tabs.background_tabs: target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg else: target_mapping[Target.tab] = usertypes.ClickTarget.tab @@ -421,9 +421,9 @@ class HintManager(QObject): if hint_mode == 'number': chars = '0123456789' else: - chars = config.get('hints', 'chars') - min_chars = config.get('hints', 'min-chars') - if config.get('hints', 'scatter') and hint_mode != 'number': + chars = config.val.hints.chars + min_chars = config.val.hints.min_chars + if config.val.hints.scatter and hint_mode != 'number': return self._hint_scattered(min_chars, chars, elems) else: return self._hint_linear(min_chars, chars, elems) @@ -685,7 +685,7 @@ class HintManager(QObject): Target.download, Target.normal, Target.current]: pass elif (target == Target.tab and - config.get('tabs', 'background-tabs')): + config.val.tabs.background_tabs): pass else: name = target.name.replace('_', '-') @@ -693,7 +693,7 @@ class HintManager(QObject): "target {}!".format(name)) if mode is None: - mode = config.get('hints', 'mode') + mode = config.val.hints.mode self._check_args(target, *args) self._context = HintContext() @@ -729,7 +729,7 @@ class HintManager(QObject): if len(visible) != 1: return - auto_follow = config.get('hints', 'auto-follow') + auto_follow = config.val.hints.auto_follow if auto_follow == "always": follow = True @@ -747,7 +747,7 @@ class HintManager(QObject): if follow: # apply auto-follow-timeout - timeout = config.get('hints', 'auto-follow-timeout') + timeout = config.val.hints.auto_follow_timeout keyparsers = objreg.get('keyparsers', scope='window', window=self._win_id) normal_parser = keyparsers[usertypes.KeyMode.normal] @@ -773,7 +773,7 @@ class HintManager(QObject): # element doesn't match anymore -> hide it, unless in rapid # mode and hide-unmatched-rapid-hints is false (see #1799) if (not self._context.rapid or - config.get('hints', 'hide-unmatched-rapid-hints')): + config.val.hints.hide_unmatched_rapid_hints): label.hide() except webelem.Error: pass diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index d1cd889d3..18069a003 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -85,7 +85,7 @@ class MouseEventFilter(QObject): def _handle_mouse_press(self, e): """Handle pressing of a mouse button.""" - is_rocker_gesture = (config.get('input', 'rocker-gestures') and + is_rocker_gesture = (config.val.input.rocker_gestures and e.buttons() == Qt.LeftButton | Qt.RightButton) if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture: @@ -119,7 +119,7 @@ class MouseEventFilter(QObject): return True if e.modifiers() & Qt.ControlModifier: - divider = config.get('input', 'mouse-zoom-divider') + divider = config.val.input.mouse_zoom_divider if divider == 0: return False factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) @@ -139,7 +139,7 @@ class MouseEventFilter(QObject): def _handle_context_menu(self, _e): """Suppress context menus if rocker gestures are turned on.""" - return config.get('input', 'rocker-gestures') + return config.val.input.rocker_gestures def _mousepress_insertmode_cb(self, elem): """Check if the clicked element is editable.""" @@ -157,7 +157,7 @@ class MouseEventFilter(QObject): 'click', only_if_normal=True) else: log.mouse.debug("Clicked non-editable element!") - if config.get('input', 'auto-leave-insert-mode'): + if config.val.input.auto_leave_insert_mode: modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, 'click', maybe=True) @@ -179,7 +179,7 @@ class MouseEventFilter(QObject): 'click-delayed', only_if_normal=True) else: log.mouse.debug("Clicked non-editable element (delayed)!") - if config.get('input', 'auto-leave-insert-mode'): + if config.val.input.auto_leave_insert_mode: modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, 'click-delayed', maybe=True) diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 4443c6a47..92126c6ad 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -42,7 +42,7 @@ def incdec(url, count, inc_or_dec): background: Open the link in a new background tab. window: Open the link in a new window. """ - segments = set(config.get('general', 'url-incdec-segments')) + segments = set(config.val.url_incdec_segments) try: new_url = urlutils.incdec_number(url, inc_or_dec, count, segments=segments) diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index 1bdbc7b0b..037c9c712 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -44,7 +44,7 @@ class ProxyFactory(QNetworkProxyFactory): Return: None if proxy is correct, otherwise an error message. """ - proxy = config.get('network', 'proxy') + proxy = config.val.network.proxy if isinstance(proxy, pac.PACFetcher): return proxy.fetch_error() else: @@ -59,7 +59,7 @@ class ProxyFactory(QNetworkProxyFactory): Return: A list of QNetworkProxy objects in order of preference. """ - proxy = config.get('network', 'proxy') + proxy = config.val.network.proxy if proxy is configtypes.SYSTEM_PROXY: proxies = QNetworkProxyFactory.systemProxyForQuery(query) elif isinstance(proxy, pac.PACFetcher): @@ -69,7 +69,7 @@ class ProxyFactory(QNetworkProxyFactory): for p in proxies: if p.type() != QNetworkProxy.NoProxy: capabilities = p.capabilities() - if config.get('network', 'proxy-dns-requests'): + if config.val.network.proxy_dns_requests: capabilities |= QNetworkProxy.HostNameLookupCapability else: capabilities &= ~QNetworkProxy.HostNameLookupCapability diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 71039dc2d..9aba6d335 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -368,7 +368,7 @@ class DownloadManager(downloads.AbstractDownloadManager): super().__init__(parent) self._networkmanager = networkmanager.NetworkManager( win_id=win_id, tab_id=None, - private=config.get('general', 'private-browsing'), parent=self) + private=config.val.private_browsing, parent=self) @pyqtSlot('QUrl') def get(self, url, *, user_agent=None, **kwargs): diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 652c58726..552ced168 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -278,14 +278,14 @@ def qute_history(url): return 'text/html', json.dumps(history_data(start_time)) else: if ( - config.get('content', 'allow-javascript') and + config.val.content.allow_javascript and (objects.backend == usertypes.Backend.QtWebEngine or qtutils.is_qtwebkit_ng()) ): return 'text/html', jinja.render( 'history.html', title='History', - session_interval=config.get('ui', 'history-session-interval') + session_interval=config.val.ui.history_session_interval ) else: # Get current date from query parameter, if not given choose today. diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index d400387a9..5d3ddc506 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -35,16 +35,16 @@ class CallSuper(Exception): def custom_headers(): """Get the combined custom headers.""" headers = {} - dnt = b'1' if config.get('network', 'do-not-track') else b'0' + dnt = b'1' if config.val.network.do_not_track else b'0' headers[b'DNT'] = dnt headers[b'X-Do-Not-Track'] = dnt - config_headers = config.get('network', 'custom-headers') + config_headers = config.val.network.custom_headers if config_headers is not None: for header, value in config_headers.items(): headers[header.encode('ascii')] = value.encode('ascii') - accept_language = config.get('network', 'accept-language') + accept_language = config.val.network.accept_language if accept_language is not None: headers[b'Accept-Language'] = accept_language.encode('ascii') @@ -72,7 +72,7 @@ def authentication_required(url, authenticator, abort_on): def javascript_confirm(url, js_msg, abort_on): """Display a javascript confirm prompt.""" log.js.debug("confirm: {}".format(js_msg)) - if config.get('ui', 'modal-js-dialog'): + if config.val.ui.modal_js_dialog: raise CallSuper msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), @@ -86,9 +86,9 @@ def javascript_confirm(url, js_msg, abort_on): def javascript_prompt(url, js_msg, default, abort_on): """Display a javascript prompt.""" log.js.debug("prompt: {}".format(js_msg)) - if config.get('ui', 'modal-js-dialog'): + if config.val.ui.modal_js_dialog: raise CallSuper - if config.get('content', 'ignore-javascript-prompt'): + if config.val.content.ignore_javascript_prompt: return (False, "") msg = '{} asks:
{}'.format(html.escape(url.toDisplayString()), @@ -107,10 +107,10 @@ def javascript_prompt(url, js_msg, default, abort_on): def javascript_alert(url, js_msg, abort_on): """Display a javascript alert.""" log.js.debug("alert: {}".format(js_msg)) - if config.get('ui', 'modal-js-dialog'): + if config.val.ui.modal_js_dialog: raise CallSuper - if config.get('content', 'ignore-javascript-alert'): + if config.val.content.ignore_javascript_alert: return msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), @@ -129,7 +129,7 @@ def ignore_certificate_errors(url, errors, abort_on): Return: True if the error should be ignored, False otherwise. """ - ssl_strict = config.get('network', 'ssl-strict') + ssl_strict = config.val.network.ssl_strict log.webview.debug("Certificate errors {!r}, strict {}".format( errors, ssl_strict)) @@ -233,7 +233,7 @@ def get_tab(win_id, target): def get_user_stylesheet(): """Get the combined user-stylesheet.""" - filename = config.get('ui', 'user-stylesheet') + filename = config.val.ui.user_stylesheet if filename is None: css = '' @@ -241,7 +241,7 @@ def get_user_stylesheet(): with open(filename, 'r', encoding='utf-8') as f: css = f.read() - if config.get('ui', 'hide-scrollbar'): + if config.val.ui.hide_scrollbar: css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }' return css diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 149300111..401a3bb93 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -182,7 +182,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # at least a classid attribute. Oh, and let's hope images/... # DON'T have a classid attribute. HTML sucks. log.webelem.debug(" clicked.".format(objtype)) - return config.get('input', 'insert-mode-on-plugins') + return config.val.input.insert_mode_on_plugins else: # Image/Audio/... return False @@ -247,7 +247,7 @@ class AbstractWebElement(collections.abc.MutableMapping): return self.is_writable() elif tag in ['embed', 'applet']: # Flash/Java/... - return config.get('input', 'insert-mode-on-plugins') and not strict + return config.val.input.insert_mode_on_plugins and not strict elif tag == 'object': return self._is_editable_object() and not strict elif tag in ['div', 'pre']: @@ -329,7 +329,7 @@ class AbstractWebElement(collections.abc.MutableMapping): usertypes.ClickTarget.tab: Qt.ControlModifier, usertypes.ClickTarget.tab_bg: Qt.ControlModifier, } - if config.get('tabs', 'background-tabs'): + if config.val.tabs.background_tabs: modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier else: modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 0cb2d446e..5d9dc8a70 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -63,6 +63,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): for header, value in shared.custom_headers(): info.setHttpHeader(header, value) - user_agent = config.get('network', 'user-agent') + user_agent = config.val.network.user_agent if user_agent is not None: info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 493a2a687..00f4ec423 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -163,7 +163,7 @@ def _set_user_agent(profile): per-domain user agents), but this one still gets used for things like window.navigator.userAgent in JS. """ - user_agent = config.get('network', 'user-agent') + user_agent = config.val.network.user_agent profile.setHttpUserAgent(user_agent) @@ -212,7 +212,7 @@ def init(args): # We need to do this here as a WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-58650 if not qtutils.version_check('5.9'): - PersistentCookiePolicy().set(config.get('content', 'cookies-store')) + PersistentCookiePolicy().set(config.val.content.cookies_store) Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) websettings.init_mappings(MAPPINGS) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index fd6fc99cb..b650df5a7 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -80,7 +80,7 @@ class WebEngineView(QWebEngineView): The new QWebEngineView object. """ debug_type = debug.qenum_key(QWebEnginePage, wintype) - background_tabs = config.get('tabs', 'background-tabs') + background_tabs = config.val.tabs.background_tabs log.webview.debug("createWindow with type {}, background_tabs " "{}".format(debug_type, background_tabs)) @@ -139,7 +139,7 @@ class WebEnginePage(QWebEnginePage): @config.change_filter('colors', 'webpage.bg') def _set_bg_color(self): - col = config.get('colors', 'webpage.bg') + col = config.val.colors.webpage.bg if col is None: col = self._theme_color self.setBackgroundColor(col) @@ -277,7 +277,7 @@ class WebEnginePage(QWebEnginePage): def javaScriptConsoleMessage(self, level, msg, line, source): """Log javascript messages to qutebrowser's log.""" # FIXME:qtwebengine maybe unify this in the tab api somehow? - setting = config.get('general', 'log-javascript-console') + setting = config.val.log_javascript_console if setting == 'none': return diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index ae717c956..6f2478c37 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -45,7 +45,7 @@ class DiskCache(QNetworkDiskCache): @config.change_filter('storage', 'cache-size') def _set_cache_size(self): """Set the cache size based on the config.""" - size = config.get('storage', 'cache-size') + size = config.val.storage.cache_size if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py index 79f7a67fa..2c9072f7d 100644 --- a/qutebrowser/browser/webkit/cookies.py +++ b/qutebrowser/browser/webkit/cookies.py @@ -50,7 +50,7 @@ class RAMCookieJar(QNetworkCookieJar): Return: True if one or more cookies are set for 'url', otherwise False. """ - if config.get('content', 'cookies-accept') == 'never': + if config.val.content.cookies_accept == 'never': return False else: self.changed.emit() @@ -77,7 +77,7 @@ class CookieJar(RAMCookieJar): objreg.get('config').changed.connect(self.cookies_store_changed) objreg.get('save-manager').add_saveable( 'cookies', self.save, self.changed, - config_opt=('content', 'cookies-store')) + config_opt='content.cookies_store') def parse_cookies(self): """Parse cookies from lineparser and store them.""" @@ -108,7 +108,7 @@ class CookieJar(RAMCookieJar): @config.change_filter('content', 'cookies-store') def cookies_store_changed(self): """Delete stored cookies if cookies-store changed.""" - if not config.get('content', 'cookies-store'): + if not config.val.content.cookies_store: self._lineparser.data = [] self._lineparser.save() self.changed.emit() diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 9d4c1b317..c2ad03a6b 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -274,7 +274,7 @@ class NetworkManager(QNetworkAccessManager): # altogether. reply.netrc_used = True try: - net = netrc.netrc(config.get('network', 'netrc-file')) + net = netrc.netrc(config.val.network.netrc_file) authenticators = net.authenticators(reply.url().host()) if authenticators is not None: (user, _account, password) = authenticators @@ -338,7 +338,7 @@ class NetworkManager(QNetworkAccessManager): def set_referer(self, req, current_url): """Set the referer header.""" - referer_header_conf = config.get('network', 'referer-header') + referer_header_conf = config.val.network.referer_header try: if referer_header_conf == 'never': diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index b184f9905..522847511 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -168,7 +168,7 @@ class WebKitElement(webelem.AbstractWebElement): if width > 1 and height > 1: # fix coordinates according to zoom level zoom = self._elem.webFrame().zoomFactor() - if not config.get('ui', 'zoom-text-only'): + if not config.val.ui.zoom_text_only: rect["left"] *= zoom rect["top"] *= zoom width *= zoom diff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py index 9a056a896..2be95cb3d 100644 --- a/qutebrowser/browser/webkit/webkitinspector.py +++ b/qutebrowser/browser/webkit/webkitinspector.py @@ -36,7 +36,7 @@ class WebKitInspector(inspector.AbstractWebInspector): self._set_widget(qwebinspector) def inspect(self, page): - if not config.get('general', 'developer-extras'): + if not config.val.developer_extras: raise inspector.WebInspectorError( "Please enable developer-extras before using the " "webinspector!") diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index dd251949f..9da142932 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -132,7 +132,7 @@ def init(_args): QWebSettings.setOfflineStoragePath( os.path.join(data_path, 'offline-storage')) - if (config.get('general', 'private-browsing') and + if (config.val.private_browsing and not qtutils.version_check('5.4.2')): # WORKAROUND for https://codereview.qt-project.org/#/c/108936/ # Won't work when private browsing is not enabled globally, but that's diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index acebf5cd3..beb47ea09 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -277,7 +277,7 @@ class BrowserPage(QWebPage): reply.finished.connect(functools.partial( self.display_content, reply, 'image/jpeg')) elif (mimetype in ['application/pdf', 'application/x-pdf'] and - config.get('content', 'enable-pdfjs')): + config.val.content.enable_pdfjs): # Use pdf.js to display the page self._show_pdfjs(reply) else: @@ -384,7 +384,7 @@ class BrowserPage(QWebPage): def userAgentForUrl(self, url): """Override QWebPage::userAgentForUrl to customize the user agent.""" - ua = config.get('network', 'user-agent') + ua = config.val.network.user_agent if ua is None: return super().userAgentForUrl(url) else: diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 99980b4b1..3daafd8ec 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -110,7 +110,7 @@ class WebView(QWebView): @config.change_filter('colors', 'webpage.bg') def _set_bg_color(self): """Set the webpage background color as configured.""" - col = config.get('colors', 'webpage.bg') + col = config.val.colors.webpage.bg palette = self.palette() if col is None: col = self.style().standardPalette().color(QPalette.Base) @@ -285,7 +285,7 @@ class WebView(QWebView): This is implemented here as we don't need it for QtWebEngine. """ if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier: - background_tabs = config.get('tabs', 'background-tabs') + background_tabs = config.val.tabs.background_tabs if e.modifiers() & Qt.ShiftModifier: background_tabs = not background_tabs if background_tabs: diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 69a36f400..03d6bbffc 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -417,7 +417,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False): lambda cmd: log.commands.debug("Got userscript command: {}".format(cmd))) runner.got_cmd.connect(commandrunner.run_safely) - user_agent = config.get('network', 'user-agent') + user_agent = config.val.network.user_agent if user_agent is not None: env['QUTE_USER_AGENT'] = user_agent diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 96d937829..38302fa5e 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -194,7 +194,7 @@ class Completer(QObject): if maxsplit is None: text = self._quote(text) model = self._model() - if model.count() == 1 and config.get('completion', 'quick-complete'): + if model.count() == 1 and config.val.completion.quick_complete: # If we only have one item, we want to apply it immediately # and go on to the next part. self._change_completed_part(text, before, after, immediate=True) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index 1d5dfadf0..f17a77cbf 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -156,7 +156,7 @@ class CompletionItemDelegate(QStyledItemDelegate): try: self._painter.setPen(config.get('colors', option)) except configexc.NoOptionError: - self._painter.setPen(config.get('colors', 'completion.fg')) + self._painter.setPen(config.val.colors.completion.fg) ctx = QAbstractTextDocumentLayout.PaintContext() ctx.palette.setColor(QPalette.Text, self._painter.pen().color()) if clip.isValid(): @@ -208,7 +208,7 @@ class CompletionItemDelegate(QStyledItemDelegate): else: self._doc.setHtml( '{}'.format( - html.escape(config.get('fonts', 'completion.category')), + html.escape(config.val.fonts.completion.category), html.escape(self._opt.text))) def _draw_focus_rect(self): diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 490fcd6c0..7fc5f36c9 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -85,13 +85,13 @@ class CompletionView(QTreeView): } QTreeView QScrollBar { - width: {{ config.get('completion', 'scrollbar-width') }}px; + width: {{ config.val.completion.scrollbar_width }}px; background: {{ color['completion.scrollbar.bg'] }}; } QTreeView QScrollBar::handle { background: {{ color['completion.scrollbar.fg'] }}; - border: {{ config.get('completion', 'scrollbar-padding') }}px solid + border: {{ config.val.completion.scrollbar_padding }}px solid {{ color['completion.scrollbar.bg'] }}; min-height: 10px; } @@ -257,9 +257,9 @@ class CompletionView(QTreeView): count = self.model().count() if count == 0: self.hide() - elif count == 1 and config.get('completion', 'quick-complete'): + elif count == 1 and config.val.completion.quick_complete: self.hide() - elif config.get('completion', 'show') == 'auto': + elif config.val.completion.show == 'auto': self.show() def set_model(self, model, pattern=None): @@ -288,7 +288,7 @@ class CompletionView(QTreeView): if old_model is not None: old_model.deleteLater() - if (config.get('completion', 'show') == 'always' and + if (config.val.completion.show == 'always' and model.count() > 0): self.show() else: @@ -306,7 +306,7 @@ class CompletionView(QTreeView): def _maybe_update_geometry(self): """Emit the update_geometry signal if the config says so.""" - if config.get('completion', 'shrink'): + if config.val.completion.shrink: self.update_geometry.emit() @pyqtSlot() @@ -321,14 +321,14 @@ class CompletionView(QTreeView): def sizeHint(self): """Get the completion size according to the config.""" # Get the configured height/percentage. - confheight = str(config.get('completion', 'height')) + confheight = str(config.val.completion.height) if confheight.endswith('%'): perc = int(confheight.rstrip('%')) height = self.window().height() * perc / 100 else: height = int(confheight) # Shrink to content size if needed and shrinking is enabled - if config.get('completion', 'shrink'): + if config.val.completion.shrink: contents_height = ( self.viewportSizeHint().height() + self.horizontalScrollBar().sizeHint().height()) diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index 98f68c08c..b52b50ff1 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -68,7 +68,7 @@ class UrlCompletionModel(base.BaseCompletionModel): bookmark_manager.removed.connect(self.on_bookmark_removed) self._history = objreg.get('web-history') - self._max_history = config.get('completion', 'web-history-max-items') + self._max_history = config.val.completion.web_history_max_items history = utils.newest_slice(self._history, self._max_history) for entry in history: if not entry.redirect: @@ -80,7 +80,7 @@ class UrlCompletionModel(base.BaseCompletionModel): def _fmt_atime(self, atime): """Format an atime to a human-readable string.""" - fmt = config.get('completion', 'timestamp-format') + fmt = config.val.completion.timestamp_format if fmt is None: return '' try: diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index a723d2f10..f52578040 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -50,6 +50,10 @@ from qutebrowser.utils.usertypes import Completion UNSET = object() +# FIXME:conf for new config +val = None + + class change_filter: # pylint: disable=invalid-name """Decorator to filter calls based on a config section/option matching. @@ -167,7 +171,7 @@ def _init_main_config(parent=None): save_manager = objreg.get('save-manager') save_manager.add_saveable( 'config', config_obj.save, config_obj.changed, - config_opt=('general', 'auto-save-config'), filename=filename) + config_opt='auto_save.config', filename=filename) for sect in config_obj.sections.values(): for opt in sect.values.values(): if opt.values['conf'] is None: @@ -204,7 +208,7 @@ def _init_key_config(parent): filename = os.path.join(standarddir.config(), 'keys.conf') save_manager.add_saveable( 'key-config', key_config.save, key_config.config_dirty, - config_opt=('general', 'auto-save-config'), filename=filename, + config_opt='auto_save.config', filename=filename, dirty=key_config.is_dirty) @@ -256,9 +260,11 @@ def init(parent=None): Args: parent: The parent to pass to QObjects which get initialized. """ + global val # _init_main_config(parent) configdata.init() _init_new_config(parent) + val = newconfig.val _init_key_config(parent) _init_misc() diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index 807ad0bfe..6dabf79e1 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -24,6 +24,15 @@ from PyQt5.QtCore import pyqtSignal, QObject from qutebrowser.config import configdata +from qutebrowser.utils import utils + +# An easy way to access the config from other code via config.val.foo +val = None + + +class UnknownOptionError(Exception): + + """Raised by NewConfigManager when an option is unknown.""" class SectionStub: @@ -48,16 +57,61 @@ class NewConfigManager(QObject): super().__init__(parent) self._values = {} - def _key(self, sect, opt=None): - if opt is None: - # New usage - return sect - return sect + '.' + opt - def read_defaults(self): for name, option in configdata.DATA.items(): self._values[name] = option - def get(self, section, option): - val = self._values[self._key(section, option)] + def get(self, option): + try: + val = self._values[option] + except KeyError as e: + raise UnknownOptionError(e) return val.typ.transform(val.default) + + def is_valid_prefix(self, prefix): + """Check whether the given prefix is a valid prefix for some option.""" + return any(key.startswith(prefix + '.') for key in self._values) + + +class ConfigContainer: + + """An object implementing config access via __getattr__. + + Attributes: + _manager: The ConfigManager object. + _prefix: The __getattr__ chain leading up to this object. + """ + + def __init__(self, manager, prefix=''): + self._manager = manager + self._prefix = prefix + + def __repr__(self): + return utils.get_repr(self, constructor=True, manager=self._manager, + prefix=self._prefix) + + def __getattr__(self, attr): + """Get an option or a new ConfigContainer with the added prefix. + + If we get an option which exists, we return the value for it. + If we get a part of an option name, we return a new ConfigContainer. + + Those two never overlap as configdata.py ensures there are no shadowing + options. + """ + name = self._join(attr) + if self._manager.is_valid_prefix(name): + return ConfigContainer(manager=self._manager, prefix=name) + # If it's not a valid prefix, this will raise NoOptionError. + self._manager.get(name) + + def __setattr__(self, attr, value): + if attr.startswith('_'): + return super().__setattr__(attr, value) + self._handler(self._join(attr), value) + + def _join(self, attr): + if self._prefix: + return '{}.{}'.format(self._prefix, attr) + else: + return attr diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 670cde853..897abb841 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -271,7 +271,7 @@ class BaseKeyParser(QObject): count: The count to pass. """ self._debug_log("Ambiguous match for '{}'".format(self._keystring)) - time = config.get('input', 'timeout') + time = config.val.input.timeout if time == 0: # execute immediately self.clear_keystring() diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 4ef393b03..bd0a5a1e9 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -74,7 +74,7 @@ class NormalKeyParser(keyparser.CommandKeyParser): return self.Match.none match = super()._handle_single_key(e) if match == self.Match.partial: - timeout = config.get('input', 'partial-timeout') + timeout = config.val.input.partial_timeout if timeout != 0: self._partial_timer.setInterval(timeout) self._partial_timer.timeout.connect(self._clear_partial_match) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 067b536bf..67aa9e125 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -60,7 +60,7 @@ def get_window(via_ipc, force_window=False, force_tab=False, # Initial main window return 0 - open_target = config.get('general', 'new-instance-open-target') + open_target = config.val.new_instance_open_target # Apply any target overrides, ordered by precedence if force_target is not None: @@ -98,7 +98,7 @@ def get_window(via_ipc, force_window=False, force_tab=False, def get_target_window(): """Get the target window for new tabs, or None if none exist.""" try: - win_mode = config.get('general', 'new-instance-open-target.window') + win_mode = config.val.new_instance_open_target.window if win_mode == 'last-focused': return objreg.last_focused_window() elif win_mode == 'first-opened': @@ -163,7 +163,7 @@ class MainWindow(QWidget): self._init_downloadmanager() self._downloadview = downloadview.DownloadView(self.win_id) - if config.get('general', 'private-browsing'): + if config.val.private_browsing: # This setting always trumps what's passed in. private = True else: @@ -250,7 +250,7 @@ class MainWindow(QWidget): left = (self.width() - width) / 2 if centered else 0 height_padding = 20 - status_position = config.get('ui', 'status-position') + status_position = config.val.ui.status_position if status_position == 'bottom': if self.status.isVisible(): status_height = self.status.height() @@ -341,8 +341,8 @@ class MainWindow(QWidget): self._vbox.removeWidget(self.tabbed_browser) self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self.status) - downloads_position = config.get('ui', 'downloads-position') - status_position = config.get('ui', 'status-position') + downloads_position = config.val.ui.downloads_position + status_position = config.val.ui.status_position widgets = [self.tabbed_browser] if downloads_position == 'top': @@ -536,7 +536,7 @@ class MainWindow(QWidget): if crashsignal.is_crashing: e.accept() return - confirm_quit = config.get('ui', 'confirm-quit') + confirm_quit = config.val.ui.confirm_quit tab_count = self.tabbed_browser.count() download_model = objreg.get('download-model', scope='window', window=self.win_id) diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 7d0d2b682..92bdd17ac 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -98,7 +98,7 @@ class MessageView(QWidget): @config.change_filter('ui', 'message-timeout') def _set_clear_timer_interval(self): """Configure self._clear_timer according to the config.""" - interval = config.get('ui', 'message-timeout') + interval = config.val.ui.message_timeout if interval != 0: self._clear_timer.setInterval(interval) @@ -127,7 +127,7 @@ class MessageView(QWidget): widget = Message(level, text, replace=replace, parent=self) self._vbox.addWidget(widget) widget.show() - if config.get('ui', 'message-timeout') != 0: + if config.val.ui.message_timeout != 0: self._clear_timer.start() self._messages.append(widget) self._last_text = text diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index c38a41caa..8a1ee66d4 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -233,9 +233,9 @@ class PromptContainer(QWidget): """ STYLESHEET = """ - {% set prompt_radius = config.get('ui', 'prompt-radius') %} + {% set prompt_radius = config.val.ui.prompt_radius %} QWidget#PromptContainer { - {% if config.get('ui', 'status-position') == 'top' %} + {% if config.val.ui.status_position == 'top' %} border-bottom-left-radius: {{ prompt_radius }}px; border-bottom-right-radius: {{ prompt_radius }}px; {% else %} @@ -566,7 +566,7 @@ class FilenamePrompt(_BasePrompt): self.setFocusProxy(self._lineedit) self._init_key_label() - if config.get('ui', 'prompt-filebrowser'): + if config.val.ui.prompt_filebrowser: self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @pyqtSlot(str) @@ -628,7 +628,7 @@ class FilenamePrompt(_BasePrompt): self._file_view.setModel(self._file_model) self._file_view.clicked.connect(self._insert_path) - if config.get('ui', 'prompt-filebrowser'): + if config.val.ui.prompt_filebrowser: self._vbox.addWidget(self._file_view) else: self._file_view.hide() diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index ee3665ce2..513b7ad83 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -211,7 +211,7 @@ class StatusBar(QWidget): @pyqtSlot() def maybe_hide(self): """Hide the statusbar if it's configured to do so.""" - hide = config.get('ui', 'hide-statusbar') + hide = config.val.ui.hide_statusbar tab = self._current_tab() if hide or (tab is not None and tab.data.fullscreen): self.hide() @@ -219,7 +219,7 @@ class StatusBar(QWidget): self.show() def _set_hbox_padding(self): - padding = config.get('ui', 'statusbar-padding') + padding = config.val.ui.statusbar_padding self._hbox.setContentsMargins(padding.left, 0, padding.right, 0) @pyqtProperty('QStringList') @@ -344,7 +344,7 @@ class StatusBar(QWidget): def minimumSizeHint(self): """Set the minimum height to the text height plus some padding.""" - padding = config.get('ui', 'statusbar-padding') + padding = config.val.ui.statusbar_padding width = super().minimumSizeHint().width() height = self.fontMetrics().height() + padding.top + padding.bottom return QSize(width, height) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index e0468330e..377825d6f 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -170,7 +170,7 @@ class TabbedBrowser(tabwidget.TabWidget): fields = self.get_tab_fields(idx) fields['id'] = self._win_id - fmt = config.get('ui', 'window-title-format') + fmt = config.val.ui.window_title_format self.window().setWindowTitle(fmt.format(**fields)) def _connect_tab_signals(self, tab): @@ -239,7 +239,7 @@ class TabbedBrowser(tabwidget.TabWidget): tab: The QWebView to be closed. add_undo: Whether the tab close can be undone. """ - last_close = config.get('tabs', 'last-close') + last_close = config.val.tabs.last_close count = self.count() if last_close == 'ignore' and count == 1: @@ -257,10 +257,10 @@ class TabbedBrowser(tabwidget.TabWidget): elif last_close == 'blank': self.openurl(QUrl('about:blank'), newtab=True) elif last_close == 'startpage': - url = QUrl(config.get('general', 'startpage')[0]) + url = QUrl(config.val.startpage[0]) self.openurl(url, newtab=True) elif last_close == 'default-page': - url = config.get('general', 'default-page') + url = config.val.default_page self.openurl(url, newtab=True) def _remove_tab(self, tab, *, add_undo=True, crashed=False): @@ -316,15 +316,15 @@ class TabbedBrowser(tabwidget.TabWidget): def undo(self): """Undo removing of a tab.""" # Remove unused tab which may be created after the last tab is closed - last_close = config.get('tabs', 'last-close') + last_close = config.val.tabs.last_close use_current_tab = False if last_close in ['blank', 'startpage', 'default-page']: only_one_tab_open = self.count() == 1 no_history = len(self.widget(0).history) == 1 urls = { 'blank': QUrl('about:blank'), - 'startpage': QUrl(config.get('general', 'startpage')[0]), - 'default-page': config.get('general', 'default-page'), + 'startpage': QUrl(config.val.startpage[0]), + 'default_page': config.val.default_page, } first_tab_url = self.widget(0).url() last_close_urlstr = urls[last_close].toString().rstrip('/') @@ -409,7 +409,7 @@ class TabbedBrowser(tabwidget.TabWidget): "explicit {}, idx {}".format( url, background, explicit, idx)) - if (config.get('tabs', 'tabs-are-windows') and self.count() > 0 and + if (config.val.tabs.tabs_are_windows and self.count() > 0 and not ignore_tabs_are_windows): from qutebrowser.mainwindow import mainwindow window = mainwindow.MainWindow(private=self.private) @@ -430,7 +430,7 @@ class TabbedBrowser(tabwidget.TabWidget): tab.openurl(url) if background is None: - background = config.get('tabs', 'background-tabs') + background = config.val.tabs.background_tabs if background: # Make sure the background tab has the correct initial size. # With a foreground tab, it's going to be resized correctly by the @@ -460,9 +460,9 @@ class TabbedBrowser(tabwidget.TabWidget): The index of the new tab. """ if explicit: - pos = config.get('tabs', 'new-tab-position-explicit') + pos = config.val.tabs.new_tab_position_explicit else: - pos = config.get('tabs', 'new-tab-position') + pos = config.val.tabs.new_tab_position if pos == 'prev': idx = self._tab_insert_idx_left # On first sight, we'd think we have to decrement @@ -488,8 +488,8 @@ class TabbedBrowser(tabwidget.TabWidget): @config.change_filter('tabs', 'show-favicons') def update_favicons(self): """Update favicons when config was changed.""" - show = config.get('tabs', 'show-favicons') - tabs_are_wins = config.get('tabs', 'tabs-are-windows') + show = config.val.tabs.show_favicons + tabs_are_wins = config.val.tabs.tabs_are_windows for i, tab in enumerate(self.widgets()): if show: self.setTabIcon(i, tab.icon()) @@ -517,8 +517,8 @@ class TabbedBrowser(tabwidget.TabWidget): tab.data.keep_icon = False else: self.setTabIcon(idx, QIcon()) - if (config.get('tabs', 'tabs-are-windows') and - config.get('tabs', 'show-favicons')): + if (config.val.tabs.tabs_are_windows and + config.val.tabs.show_favicons): self.window().setWindowIcon(self.default_window_icon) if idx == self.currentIndex(): self.update_window_title() @@ -582,7 +582,7 @@ class TabbedBrowser(tabwidget.TabWidget): tab: The WebView where the title was changed. icon: The new icon """ - if not config.get('tabs', 'show-favicons'): + if not config.val.tabs.show_favicons: return try: idx = self._tab_index(tab) @@ -590,7 +590,7 @@ class TabbedBrowser(tabwidget.TabWidget): # We can get signals for tabs we already deleted... return self.setTabIcon(idx, icon) - if config.get('tabs', 'tabs-are-windows'): + if config.val.tabs.tabs_are_windows: self.window().setWindowIcon(icon) @pyqtSlot(usertypes.KeyMode) @@ -643,9 +643,9 @@ class TabbedBrowser(tabwidget.TabWidget): except TabDeletedError: # We can get signals for tabs we already deleted... return - start = config.get('colors', 'tabs.indicator.start') - stop = config.get('colors', 'tabs.indicator.stop') - system = config.get('colors', 'tabs.indicator.system') + start = config.val.colors.tabs.indicator.start + stop = config.val.colors.tabs.indicator.stop + system = config.val.colors.tabs.indicator.system color = utils.interpolate_color(start, stop, perc, system) self.set_tab_indicator_color(idx, color) self.update_tab_title(idx) @@ -660,12 +660,12 @@ class TabbedBrowser(tabwidget.TabWidget): # We can get signals for tabs we already deleted... return if ok: - start = config.get('colors', 'tabs.indicator.start') - stop = config.get('colors', 'tabs.indicator.stop') - system = config.get('colors', 'tabs.indicator.system') + start = config.val.colors.tabs.indicator.start + stop = config.val.colors.tabs.indicator.stop + system = config.val.colors.tabs.indicator.system color = utils.interpolate_color(start, stop, 100, system) else: - color = config.get('colors', 'tabs.indicator.error') + color = config.val.colors.tabs.indicator.error self.set_tab_indicator_color(idx, color) self.update_tab_title(idx) if idx == self.currentIndex(): diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 2c4eb6eab..6fdbea5dd 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -74,10 +74,10 @@ class TabWidget(QTabWidget): # WORKAROUND for PyQt 5.2 return tabbar = self.tabBar() - self.setMovable(config.get('tabs', 'movable')) + self.setMovable(config.val.tabs.movable) self.setTabsClosable(False) - position = config.get('tabs', 'position') - selection_behavior = config.get('tabs', 'select-on-remove') + position = config.val.tabs.position + selection_behavior = config.val.tabs.select_on_remove self.setTabPosition(position) tabbar.vertical = position in [QTabWidget.West, QTabWidget.East] tabbar.setSelectionBehaviorOnRemove(selection_behavior) @@ -140,8 +140,8 @@ class TabWidget(QTabWidget): fields['title'] = fields['title'].replace('&', '&&') fields['index'] = idx + 1 - fmt = config.get('tabs', 'title-format') - fmt_pinned = config.get('tabs', 'title-format-pinned') + fmt = config.val.tabs.title_format + fmt_pinned = config.val.tabs.title_format_pinned if tab.data.pinned: title = '' if fmt_pinned is None else fmt_pinned.format(**fields) @@ -313,7 +313,7 @@ class TabBar(QTabBar): self._auto_hide_timer = QTimer() self._auto_hide_timer.setSingleShot(True) self._auto_hide_timer.setInterval( - config.get('tabs', 'show-switching-delay')) + config.val.tabs.show_switching_delay) self._auto_hide_timer.timeout.connect(self.maybe_hide) self.setAutoFillBackground(True) self.set_colors() @@ -340,12 +340,12 @@ class TabBar(QTabBar): def on_show_switching_delay_changed(self): """Set timer interval when tabs->show-switching-delay got changed.""" self._auto_hide_timer.setInterval( - config.get('tabs', 'show-switching-delay')) + config.val.tabs.show_switching_delay) def on_current_changed(self): """Show tab bar when current tab got changed.""" self.maybe_hide() # for fullscreen tabs - show = config.get('tabs', 'show') + show = config.val.tabs.show if show == 'switching': self.show() self._auto_hide_timer.start() @@ -353,7 +353,7 @@ class TabBar(QTabBar): @pyqtSlot() def maybe_hide(self): """Hide the tab bar if needed.""" - show = config.get('tabs', 'show') + show = config.val.tabs.show tab = self._current_tab() if (show in ['never', 'switching'] or (show == 'multiple' and self.count() == 1) or @@ -411,21 +411,21 @@ class TabBar(QTabBar): @config.change_filter('fonts', 'tabbar') def set_font(self): """Set the tab bar font.""" - self.setFont(config.get('fonts', 'tabbar')) + self.setFont(config.val.fonts.tabbar) self.set_icon_size() @config.change_filter('tabs', 'favicon-scale') def set_icon_size(self): """Set the tab bar favicon size.""" size = self.fontMetrics().height() - 2 - size *= config.get('tabs', 'favicon-scale') + size *= config.val.tabs.favicon_scale self.setIconSize(QSize(size, size)) @config.change_filter('colors', 'tabs.bg.bar') def set_colors(self): """Set the tab bar colors.""" p = self.palette() - p.setColor(QPalette.Window, config.get('colors', 'tabs.bg.bar')) + p.setColor(QPalette.Window, config.val.colors.tabs.bg.bar) self.setPalette(p) @pyqtSlot(str, str) @@ -436,7 +436,7 @@ class TabBar(QTabBar): def mousePressEvent(self, e): """Override mousePressEvent to close tabs if configured.""" - button = config.get('tabs', 'close-mouse-button') + button = config.val.tabs.close_mouse_button if (e.button() == Qt.RightButton and button == 'right' or e.button() == Qt.MiddleButton and button == 'middle'): e.accept() @@ -457,7 +457,7 @@ class TabBar(QTabBar): A QSize. """ icon = self.tabIcon(index) - padding = config.get('tabs', 'padding') + padding = config.val.tabs.padding padding_h = padding.left + padding.right padding_v = padding.top + padding.bottom if icon.isNull(): @@ -468,7 +468,7 @@ class TabBar(QTabBar): icon_size = icon.actualSize(QSize(extent, extent)) padding_h += self.style().pixelMetric( PixelMetrics.icon_padding, None, self) - indicator_width = config.get('tabs', 'indicator-width') + indicator_width = config.val.tabs.indicator_width height = self.fontMetrics().height() + padding_v width = (self.fontMetrics().width('\u2026') + icon_size.width() + padding_h + indicator_width) @@ -488,7 +488,7 @@ class TabBar(QTabBar): minimum_size = self.minimumTabSizeHint(index) height = minimum_size.height() if self.vertical: - confwidth = str(config.get('tabs', 'width')) + confwidth = str(config.val.tabs.width) if confwidth.endswith('%'): main_window = objreg.get('main-window', scope='window', window=self._win_id) @@ -507,7 +507,7 @@ class TabBar(QTabBar): # get scroll buttons as soon as needed. size = minimum_size else: - tab_width_pinned_conf = config.get('tabs', 'pinned-width') + tab_width_pinned_conf = config.val.tabs.pinned_width try: pinned = self.tab_data(index, 'pinned') @@ -596,7 +596,7 @@ class TabBar(QTabBar): Args: e: The QWheelEvent """ - if config.get('tabs', 'mousewheel-tab-switching'): + if config.val.tabs.mousewheel_tab_switching: super().wheelEvent(e) else: tabbed_browser = objreg.get('tabbed-browser', scope='window', @@ -706,7 +706,7 @@ class TabBarStyle(QCommonStyle): elif element == QStyle.CE_TabBarTabLabel: if not opt.icon.isNull() and layouts.icon.isValid(): self._draw_icon(layouts, opt, p) - alignment = (config.get('tabs', 'title-alignment') | + alignment = (config.val.tabs.title_alignment | Qt.AlignVCenter | Qt.TextHideMnemonic) self._style.drawItemText(p, layouts.text, alignment, opt.palette, opt.state & QStyle.State_Enabled, @@ -775,8 +775,8 @@ class TabBarStyle(QCommonStyle): Return: A Layout namedtuple with two QRects. """ - padding = config.get('tabs', 'padding') - indicator_padding = config.get('tabs', 'indicator-padding') + padding = config.val.tabs.padding + indicator_padding = config.val.tabs.indicator_padding text_rect = QRect(opt.rect) if not text_rect.isValid(): @@ -787,7 +787,7 @@ class TabBarStyle(QCommonStyle): text_rect.adjust(padding.left, padding.top, -padding.right, -padding.bottom) - indicator_width = config.get('tabs', 'indicator-width') + indicator_width = config.val.tabs.indicator_width if indicator_width == 0: indicator_rect = QRect() else: @@ -830,10 +830,10 @@ class TabBarStyle(QCommonStyle): icon_state = (QIcon.On if opt.state & QStyle.State_Selected else QIcon.Off) # reserve space for favicon when tab bar is vertical (issue #1968) - position = config.get('tabs', 'position') + position = config.val.tabs.position if (opt.icon.isNull() and position in [QTabWidget.East, QTabWidget.West] and - config.get('tabs', 'show-favicons')): + config.val.tabs.show_favicons): tab_icon_size = icon_size else: actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state) diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 963076c21..f7e06c56b 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -105,7 +105,7 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): @config.change_filter('fonts', 'debug-console') def update_font(self): """Set the correct font.""" - self.setFont(config.get('fonts', 'debug-console')) + self.setFont(config.val.fonts.debug_console) class ConsoleTextEdit(QTextEdit): @@ -126,7 +126,7 @@ class ConsoleTextEdit(QTextEdit): @config.change_filter('fonts', 'debug-console') def update_font(self): """Update font when config changed.""" - self.setFont(config.get('fonts', 'debug-console')) + self.setFont(config.val.fonts.debug_console) def append_text(self, text): """Append new text and scroll output to bottom. diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index b670f80ed..115796259 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -432,7 +432,7 @@ class ExceptionCrashDialog(_CrashDialog): self._chk_log = QCheckBox("Include a debug log in the report", checked=True) try: - if config.get('general', 'private-browsing'): + if config.val.private_browsing: self._chk_log.setChecked(False) except Exception: log.misc.exception("Error while checking private browsing mode") @@ -524,7 +524,7 @@ class FatalCrashDialog(_CrashDialog): "accessed pages in the report.", checked=True) try: - if config.get('general', 'private-browsing'): + if config.val.private_browsing: self._chk_history.setChecked(False) except Exception: log.misc.exception("Error while checking private browsing mode") diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 58a08daf1..dba12d529 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -75,7 +75,7 @@ class ExternalEditor(QObject): try: if exitcode != 0: return - encoding = config.get('general', 'editor-encoding') + encoding = config.val.editor_encoding try: with open(self._file.name, 'r', encoding=encoding) as f: text = f.read() @@ -102,7 +102,7 @@ class ExternalEditor(QObject): if self._text is not None: raise ValueError("Already editing a file!") self._text = text - encoding = config.get('general', 'editor-encoding') + encoding = config.val.editor_encoding try: # Close while the external process is running, as otherwise systems # with exclusive write access (e.g. Windows) may fail to update @@ -120,7 +120,7 @@ class ExternalEditor(QObject): self._proc = guiprocess.GUIProcess(what='editor', parent=self) self._proc.finished.connect(self.on_proc_closed) self._proc.error.connect(self.on_proc_error) - editor = config.get('general', 'editor') + editor = config.val.editor executable = editor[0] args = [arg.replace('{}', self._file.name) for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index b42612b1b..2dfede7b4 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -51,7 +51,7 @@ class KeyHintView(QLabel): color: {{ color['keyhint.fg'] }}; background-color: {{ color['keyhint.bg'] }}; padding: 6px; - {% if config.get('ui', 'status-position') == 'top' %} + {% if config.val.ui.status_position == 'top' %} border-bottom-right-radius: 6px; {% else %} border-top-right-radius: 6px; @@ -90,7 +90,7 @@ class KeyHintView(QLabel): self.hide() return - blacklist = config.get('ui', 'keyhint-blacklist') or [] + blacklist = config.val.ui.keyhint_blacklist or [] keyconf = objreg.get('key-config') def blacklisted(keychain): @@ -107,9 +107,9 @@ class KeyHintView(QLabel): return # delay so a quickly typed keychain doesn't display hints - self._show_timer.setInterval(config.get('ui', 'keyhint-delay')) + self._show_timer.setInterval(config.val.ui.keyhint_delay) self._show_timer.start() - suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix')) + suffix_color = html.escape(config.val.colors.keyhint.fg.suffix) text = '' for key, cmd in bindings: diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index 509e5489a..57755b4cd 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -38,9 +38,8 @@ class Saveable: _dirty: Whether the saveable was changed since the last save. _save_handler: The function to call to save this Saveable. _save_on_exit: Whether to always save this saveable on exit. - _config_opt: A (section, option) tuple of a config option which decides - whether to auto-save or not. None if no such option - exists. + _config_opt: A config option which decides whether to auto-save or not. + None if no such option exists. _filename: The filename of the underlying file. """ @@ -82,7 +81,7 @@ class Saveable: force: Force saving, no matter what. """ if (self._config_opt is not None and - (not config.get(*self._config_opt)) and + (not config.get(self._config_opt)) and (not explicit) and (not force)): if not silent: log.save.debug("Not saving {name} because autosaving has been " @@ -130,7 +129,7 @@ class SaveManager(QObject): @config.change_filter('general', 'auto-save-interval') def set_autosave_interval(self): """Set the auto-save interval.""" - interval = config.get('general', 'auto-save-interval') + interval = config.val.auto_save.interval if interval == 0: self._save_timer.stop() else: @@ -145,8 +144,7 @@ class SaveManager(QObject): name: The name to use. save: The function to call to save this saveable. changed: The signal emitted when this saveable changed. - config_opt: A (section, option) tuple deciding whether to auto-save - or not. + config_opt: An option deciding whether to auto-save or not. filename: The filename of the underlying file, so we can force saving if it doesn't exist. dirty: Whether the saveable is already dirty. diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 5ce4ee66d..3bbee29f9 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -259,7 +259,7 @@ class SessionManager(QObject): object. """ if name is default: - name = config.get('general', 'session-default-name') + name = config.val.session_default_name if name is None: if self._current is not None: name = self._current diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 4cd0e94d0..24bbfbc9c 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -99,7 +99,7 @@ def _get_search_url(txt): engine, term = _parse_search_term(txt) assert term if engine is None: - template = config.get('searchengines', 'DEFAULT') + template = config.val.searchengines.DEFAULT else: template = config.get('searchengines', engine) url = qurl_from_user_input(template.format(urllib.parse.quote(term))) @@ -194,7 +194,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True, url = qurl_from_user_input(urlstr) log.url.debug("Converting fuzzy term {!r} to URL -> {}".format( urlstr, url.toDisplayString())) - if do_search and config.get('general', 'auto-search') and urlstr: + if do_search and config.val.auto_search and urlstr: qtutils.ensure_valid(url) else: if not url.isValid(): @@ -239,7 +239,7 @@ def is_url(urlstr): Return: True if it is a valid URL, False otherwise. """ - autosearch = config.get('general', 'auto-search') + autosearch = config.val.auto_search log.url.debug("Checking if {!r} is a URL (autosearch={}).".format( urlstr, autosearch)) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 0b527637e..114be6a52 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -836,7 +836,7 @@ def open_file(filename, cmdline=None): from qutebrowser.config import config # the default program to open downloads with - will be empty string # if we want to use the default - override = config.get('general', 'default-open-dispatcher') + override = config.val.default_open_dispatcher # precedence order: cmdline > default-open-dispatcher > openUrl From 7e52eb7b0e744f3391a4b8c8a85434182894a045 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 12 Jun 2017 15:12:32 +0200 Subject: [PATCH 009/516] Initial work on new configdata --- qutebrowser/config/configtypes.py | 257 +++++++++++++++++------------- 1 file changed, 149 insertions(+), 108 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 6f11ab1ef..4c571fcb3 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -134,11 +134,14 @@ class BaseType: """Get the type's valid values for documentation.""" return self.valid_values - def _basic_validation(self, value): + def _basic_validation(self, value, pytype=None): """Do some basic validation for the value (empty, non-printable chars). + Also does a Python typecheck on the value (if coming from YAML/Python). + Arguments: value: The value to check. + pytype: If given, a Python type to check the value against. """ if not value: if self.none_ok: @@ -146,29 +149,17 @@ class BaseType: else: raise configexc.ValidationError(value, "may not be empty!") - if any(ord(c) < 32 or ord(c) == 0x7f for c in value): - raise configexc.ValidationError(value, "may not contain " - "unprintable chars!") + if isinstance(value, str): + if any(ord(c) < 32 or ord(c) == 0x7f for c in value): + raise configexc.ValidationError(value, "may not contain " + "unprintable chars!") - def transform(self, value): - """Transform the setting value. + if pytype is not None and not isinstance(value, pytype): + raise configexc.ValidationError( + value, "expected a value of type {} but got {}".format( + pytype, type(value))) - This method can assume the value is indeed a valid value. - - The default implementation returns the original value. - - Args: - value: The original string value. - - Return: - The transformed value. - """ - if not value: - return None - else: - return value - - def validate(self, value): + def _validate_valid_values(self, value): """Validate value against possible values. The default implementation checks the value against self.valid_values @@ -177,7 +168,7 @@ class BaseType: Args: value: The value to validate. """ - self._basic_validation(value) + # FIXME:conf still needed? if not value: return if self.valid_values is not None: @@ -189,6 +180,45 @@ class BaseType: raise NotImplementedError("{} does not implement validate.".format( self.__class__.__name__)) + def from_str(self, value): + """Get the setting value from a string. + + Args: + value: The original string value. + + Return: + The transformed value. + """ + raise NotImplementedError + + def from_py(self, value): + """Get the setting value from a Python value. + + Args: + value: The value we got from Python/YAML. + + Return: + The transformed value. + + Raise: + configexc.ValidationError if the value was invalid. + """ + raise NotImplementedError + + def to_str(self, value): + """Get a string from the setting value. + + The resulting string should be parseable again by from_str. + """ + raise NotImplementedError + + # def to_py(self, value): + # """Get a Python/YAML value from the setting value. + + # The resulting value should be parseable again by from_py. + # """ + # raise NotImplementedError + def complete(self): """Return a list of possible values for completion. @@ -223,19 +253,25 @@ class MappingType(BaseType): MAPPING = {} - def __init__(self, none_ok=False, - valid_values=None): + def __init__(self, none_ok=False, valid_values=None): super().__init__(none_ok) self.valid_values = valid_values - def validate(self, value): - super().validate(value.lower()) - - def transform(self, value): + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: return None + self._validate_valid_values(value.lower()) return self.MAPPING[value.lower()] + def from_str(self, value): + return self.from_py(value) + + def to_str(self, value): + reverse_mapping = {v: k for k, v in self.MAPPING.items()} + assert len(self.MAPPING) == len(reverse_mapping) + return reverse_mapping[value] + class String(BaseType): @@ -265,16 +301,11 @@ class String(BaseType): self.forbidden = forbidden self._completions = completions - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: - return - - if self.valid_values is not None: - if value not in self.valid_values: - raise configexc.ValidationError( - value, - "valid values: {}".format(', '.join(self.valid_values))) + return None + self._validate_valid_values(value) if self.forbidden is not None and any(c in value for c in self.forbidden): @@ -287,6 +318,11 @@ class String(BaseType): raise configexc.ValidationError(value, "must be at most {} chars " "long!".format(self.maxlen)) + return value + + def from_str(self, value): + return self.from_py(value) + def complete(self): if self._completions is not None: return self._completions @@ -298,16 +334,18 @@ class UniqueCharString(String): """A string which may not contain duplicate chars.""" - def validate(self, value): - super().validate(value) + def from_py(self, value): + value = super().from_py(value) if not value: - return + return None # Check for duplicate values if len(set(value)) != len(value): raise configexc.ValidationError( value, "String contains duplicate values!") + return value + class List(BaseType): @@ -329,23 +367,23 @@ class List(BaseType): def get_valid_values(self): return self.valtype.get_valid_values() - def transform(self, value): + def from_str(self, value): + try: + json_val = json.loads(value) + except ValueError as e: + raise configexc.ValidationError(value, str(e)) + return self.from_py(json_val) + + def from_py(self, value): + self._basic_validation(value, pytype=list) if not value: return None - else: - return [self.valtype.transform(v.strip()) - for v in value.split(',')] - def validate(self, value): - self._basic_validation(value) - if not value: - return - vals = value.split(',') - if self.length is not None and len(vals) != self.length: + if self.length is not None and len(value) != self.length: raise configexc.ValidationError(value, "Exactly {} values need to " "be set!".format(self.length)) - for val in vals: - self.valtype.validate(val.strip()) + + return [self.valtype.from_py(v) for v in value] class FlagList(List): @@ -364,19 +402,13 @@ class FlagList(List): super().__init__(BaseType(), none_ok) self.valtype.valid_values = valid_values - def validate(self, value): - if self.valtype.valid_values is not None: - super().validate(value) - else: - self._basic_validation(value) - if not value: - return - vals = super().transform(value) - + def from_py(self, value): + vals = super().from_py(value) # Check for duplicate values if len(set(vals)) != len(vals): raise configexc.ValidationError( value, "List contains duplicate values!") + return vals def complete(self): valid_values = self.valtype.valid_values @@ -407,17 +439,18 @@ class Bool(BaseType): super().__init__(none_ok) self.valid_values = ValidValues('true', 'false') - def transform(self, value): - if not value: - return None - else: - return BOOLEAN_STATES[value.lower()] + def from_py(self, value): + self._basic_validation(value, pytype=bool) + return value - def validate(self, value): + def from_str(self, value): self._basic_validation(value) if not value: - return - elif value.lower() not in BOOLEAN_STATES: + return None + + try: + return BOOLEAN_STATES[value.lower()] + except KeyError: raise configexc.ValidationError(value, "must be a boolean!") @@ -429,17 +462,15 @@ class BoolAsk(Bool): super().__init__(none_ok) self.valid_values = ValidValues('true', 'false', 'ask') - def transform(self, value): - if value.lower() == 'ask': + def from_py(self, value): + # basic validation unneeded if it's == 'ask' and done by Bool if we call + # super().from_py + if isinstance(value, str) and value.lower() == 'ask': return 'ask' - else: - return super().transform(value) + return super().from_py(value) - def validate(self, value): - if value.lower() == 'ask': - return - else: - super().validate(value) + def from_str(self, value): + return self.from_py(value) class Int(BaseType): @@ -470,26 +501,24 @@ class Int(BaseType): assert isinstance(value, int), value return value - def transform(self, value): - if not value: - return None - else: - return int(value) - - def validate(self, value): - self._basic_validation(value) - if not value: - return + def from_str(self, value): try: intval = int(value) except ValueError: raise configexc.ValidationError(value, "must be an integer!") - if self.minval is not None and intval < self.minval: + return self.from_py(intval) + + def from_py(self, value): + self._basic_validation(value, pytype=int) + if not value: + return value + if self.minval is not None and value < self.minval: raise configexc.ValidationError(value, "must be {} or " "bigger!".format(self.minval)) - if self.maxval is not None and intval > self.maxval: + if self.maxval is not None and value > self.maxval: raise configexc.ValidationError(value, "must be {} or " "smaller!".format(self.maxval)) + return value class Float(BaseType): @@ -501,34 +530,46 @@ class Float(BaseType): maxval: Maximum value (inclusive). """ + # FIXME:conf inherit Int/Float + def __init__(self, minval=None, maxval=None, none_ok=False): super().__init__(none_ok) - if maxval is not None and minval is not None and maxval < minval: - raise ValueError("minval ({}) needs to be <= maxval ({})!".format( - minval, maxval)) - self.minval = minval - self.maxval = maxval + self.minval = self._parse_limit(minval) + self.maxval = self._parse_limit(maxval) + if self.maxval is not None and self.minval is not None: + if self.maxval < self.minval: + raise ValueError("minval ({}) needs to be <= maxval ({})!" + .format(self.minval, self.maxval)) - def transform(self, value): - if not value: - return None + def _parse_limit(self, value): + if value == 'maxint': + return qtutils.MAXVALS['int'] + elif value == 'maxint64': + return qtutils.MAXVALS['int64'] else: - return float(value) + if value is not None: + assert isinstance(value, int), value + return value - def validate(self, value): - self._basic_validation(value) - if not value: - return + def from_str(self, value): try: - floatval = float(value) + intval = int(value) except ValueError: - raise configexc.ValidationError(value, "must be a float!") - if self.minval is not None and floatval < self.minval: + raise configexc.ValidationError(value, "must be an integer!") + return self.from_py(intval) + + def from_py(self, value): + self._basic_validation(value, pytype=int) + if not value: + return value + if self.minval is not None and value < self.minval: raise configexc.ValidationError(value, "must be {} or " "bigger!".format(self.minval)) - if self.maxval is not None and floatval > self.maxval: + if self.maxval is not None and value > self.maxval: raise configexc.ValidationError(value, "must be {} or " "smaller!".format(self.maxval)) + return value + class Perc(BaseType): From 921d02e4d349bdb965e13abc2eac1a3138f0483d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 10:20:46 +0200 Subject: [PATCH 010/516] First portion of configtypes refactoring --- qutebrowser/config/configdata.yml | 5 +- qutebrowser/config/configtypes.py | 712 ++++++++++++++---------------- 2 files changed, 342 insertions(+), 375 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 69298b934..93a802c19 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -454,6 +454,7 @@ content.custom_headers: name: Dict keytype: String valtype: String + encoding: ascii none_ok: true desc: Set custom headers for qutebrowser HTTP requests. @@ -1178,7 +1179,7 @@ searchengines: DEFAULT: https://duckduckgo.com/?q={} type: name: Dict - keytype: SearchEngineName + keytype: String valtype: SearchEngineUrl desc: FIXME @@ -1512,7 +1513,7 @@ colors.keyhint.fg: colors.keyhint.suffix.fg: default: '#FFFF00' - type: CssColor + type: QssColor desc: Highlight color for keys to complete the current keychain colors.keyhint.bg: diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 4c571fcb3..e0b1c7084 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -45,33 +45,6 @@ BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, '0': False, 'no': False, 'false': False, 'off': False} -def _validate_regex(pattern, flags): - """Check if the given regex is valid. - - This is more complicated than it could be since there's a warning on - invalid escapes with newer Python versions, and we want to catch that case - and treat it as invalid. - """ - with warnings.catch_warnings(record=True) as recorded_warnings: - warnings.simplefilter('always') - try: - re.compile(pattern, flags) - except re.error as e: - raise configexc.ValidationError( - pattern, "must be a valid regex - " + str(e)) - except RuntimeError: # pragma: no cover - raise configexc.ValidationError( - pattern, "must be a valid regex - recursion depth exceeded") - - for w in recorded_warnings: - if (issubclass(w.category, DeprecationWarning) and - str(w.message).startswith('bad escape')): - raise configexc.ValidationError( - pattern, "must be a valid regex - " + str(w.message)) - else: - warnings.warn(w.message) - - class ValidValues: """Container for valid values for a given type. @@ -183,13 +156,17 @@ class BaseType: def from_str(self, value): """Get the setting value from a string. + By default this tries to invoke from_py(), so if from_py() accepts a + string rather than something more sophisticated, this doesn't need to be + implemented. + Args: value: The original string value. Return: The transformed value. """ - raise NotImplementedError + return self.from_py(value) def from_py(self, value): """Get the setting value from a Python value. @@ -264,9 +241,6 @@ class MappingType(BaseType): self._validate_valid_values(value.lower()) return self.MAPPING[value.lower()] - def from_str(self, value): - return self.from_py(value) - def to_str(self, value): reverse_mapping = {v: k for k, v in self.MAPPING.items()} assert len(self.MAPPING) == len(reverse_mapping) @@ -320,9 +294,6 @@ class String(BaseType): return value - def from_str(self, value): - return self.from_py(value) - def complete(self): if self._completions is not None: return self._completions @@ -367,22 +338,29 @@ class List(BaseType): def get_valid_values(self): return self.valtype.get_valid_values() + def _validate_list(self, value): + if self.length is not None and len(value) != self.length: + raise configexc.ValidationError(value, "Exactly {} values need to " + "be set!".format(self.length)) + def from_str(self, value): try: json_val = json.loads(value) except ValueError as e: raise configexc.ValidationError(value, str(e)) - return self.from_py(json_val) + # Can't use self.from_py here because we don't want to call from_py on + # the values. + self._basic_validation(json_val, pytype=list) + if not json_val: + return None + + return [self.valtype.from_str(v) for v in json_val] def from_py(self, value): self._basic_validation(value, pytype=list) if not value: return None - if self.length is not None and len(value) != self.length: - raise configexc.ValidationError(value, "Exactly {} values need to " - "be set!".format(self.length)) - return [self.valtype.from_py(v) for v in value] @@ -469,13 +447,10 @@ class BoolAsk(Bool): return 'ask' return super().from_py(value) - def from_str(self, value): - return self.from_py(value) +class _Numeric(BaseType): -class Int(BaseType): - - """Base class for an integer setting. + """Base class for Float/Int. Attributes: minval: Minimum value (inclusive). @@ -484,136 +459,96 @@ class Int(BaseType): def __init__(self, minval=None, maxval=None, none_ok=False): super().__init__(none_ok) - self.minval = self._parse_limit(minval) - self.maxval = self._parse_limit(maxval) + self.minval = self._parse_bound(minval) + self.maxval = self._parse_bound(maxval) if self.maxval is not None and self.minval is not None: if self.maxval < self.minval: raise ValueError("minval ({}) needs to be <= maxval ({})!" .format(self.minval, self.maxval)) - def _parse_limit(self, value): - if value == 'maxint': + def _parse_bound(self, bound): + """Get a numeric bound from a string like 'maxint'.""" + if bound == 'maxint': return qtutils.MAXVALS['int'] - elif value == 'maxint64': + elif bound == 'maxint64': return qtutils.MAXVALS['int64'] else: - if value is not None: - assert isinstance(value, int), value - return value + if bound is not None: + assert isinstance(bound, (int, float)), bound + return bound + + def _validate_bounds(self, value, suffix=''): + """Validate self.minval and self.maxval.""" + if value is None: + return + elif self.minval is not None and value < self.minval: + raise configexc.ValidationError( + value, "must be {}{} or bigger!".format(self.minval, suffix)) + elif self.maxval is not None and value > self.maxval: + raise configexc.ValidationError( + value, "must be {}{} or smaller!".format(self.maxval, suffix)) + + +class Int(_Numeric): + + """Base class for an integer setting.""" def from_str(self, value): try: intval = int(value) except ValueError: raise configexc.ValidationError(value, "must be an integer!") + # FIXME:conf should we check .is_integer() here?! return self.from_py(intval) def from_py(self, value): self._basic_validation(value, pytype=int) - if not value: - return value - if self.minval is not None and value < self.minval: - raise configexc.ValidationError(value, "must be {} or " - "bigger!".format(self.minval)) - if self.maxval is not None and value > self.maxval: - raise configexc.ValidationError(value, "must be {} or " - "smaller!".format(self.maxval)) + self._validate_bounds(value) return value -class Float(BaseType): +class Float(_Numeric): - """Base class for a float setting. - - Attributes: - minval: Minimum value (inclusive). - maxval: Maximum value (inclusive). - """ - - # FIXME:conf inherit Int/Float - - def __init__(self, minval=None, maxval=None, none_ok=False): - super().__init__(none_ok) - self.minval = self._parse_limit(minval) - self.maxval = self._parse_limit(maxval) - if self.maxval is not None and self.minval is not None: - if self.maxval < self.minval: - raise ValueError("minval ({}) needs to be <= maxval ({})!" - .format(self.minval, self.maxval)) - - def _parse_limit(self, value): - if value == 'maxint': - return qtutils.MAXVALS['int'] - elif value == 'maxint64': - return qtutils.MAXVALS['int64'] - else: - if value is not None: - assert isinstance(value, int), value - return value + """Base class for a float setting.""" def from_str(self, value): try: - intval = int(value) + floatval = float(value) except ValueError: - raise configexc.ValidationError(value, "must be an integer!") - return self.from_py(intval) + raise configexc.ValidationError(value, "must be a float!") + return self.from_py(floatval) def from_py(self, value): - self._basic_validation(value, pytype=int) - if not value: - return value - if self.minval is not None and value < self.minval: - raise configexc.ValidationError(value, "must be {} or " - "bigger!".format(self.minval)) - if self.maxval is not None and value > self.maxval: - raise configexc.ValidationError(value, "must be {} or " - "smaller!".format(self.maxval)) + self._basic_validation(value, pytype=float) + self._validate_bounds(value) return value +class Perc(_Numeric): -class Perc(BaseType): + """A percentage, as a string ending with %.""" - """Percentage. - - Attributes: - minval: Minimum value (inclusive). - maxval: Maximum value (inclusive). - """ - - def __init__(self, minval=None, maxval=None, none_ok=False): - super().__init__(none_ok) - if maxval is not None and minval is not None and maxval < minval: - raise ValueError("minval ({}) needs to be <= maxval ({})!".format( - minval, maxval)) - self.minval = minval - self.maxval = maxval - - def transform(self, value): + def from_str(self, value): if not value: - return - else: - return int(value[:-1]) - - def validate(self, value): - self._basic_validation(value) - if not value: - return + return None elif not value.endswith('%'): raise configexc.ValidationError(value, "does not end with %") + try: - intval = int(value[:-1]) + floatval = float(value[:-1]) except ValueError: - raise configexc.ValidationError(value, "invalid percentage!") - if self.minval is not None and intval < self.minval: - raise configexc.ValidationError(value, "must be {}% or " - "more!".format(self.minval)) - if self.maxval is not None and intval > self.maxval: - raise configexc.ValidationError(value, "must be {}% or " - "less!".format(self.maxval)) + raise configexc.ValidationError(value, "must be a percentage!") + return self.from_py(floatval) + + def from_py(self, value): + self._basic_validation(value, pytype=float) + if not value: + return None + self._validate_bounds(value, suffix='%') + return value -class PercOrInt(BaseType): +class PercOrInt(_Numeric): """Percentage or integer. @@ -626,58 +561,69 @@ class PercOrInt(BaseType): def __init__(self, minperc=None, maxperc=None, minint=None, maxint=None, none_ok=False): - super().__init__(none_ok) - if maxint is not None and minint is not None and maxint < minint: - raise ValueError("minint ({}) needs to be <= maxint ({})!".format( - minint, maxint)) - if maxperc is not None and minperc is not None and maxperc < minperc: + super().__init__(minval=minint, maxval=maxint, none_ok=none_ok) + self.minperc = self._parse_bound(minperc) + self.maxperc = self._parse_bound(maxperc) + if (self.maxperc is not None and self.minperc is not None and + self.maxperc < self.minperc): raise ValueError("minperc ({}) needs to be <= maxperc " - "({})!".format(minperc, maxperc)) - self.minperc = minperc - self.maxperc = maxperc - self.minint = minint - self.maxint = maxint + "({})!".format(self.minperc, self.maxperc)) - def validate(self, value): - self._basic_validation(value) + def from_str(self, value): + if not value: + return None + + if value.endswith('%'): + return self.from_py(value) + + try: + floatval = float(value) + except ValueError: + raise configexc.ValidationError(value, + "must be integer or percentage!") + return self.from_py(floatval) + + def from_py(self, value): + self._basic_validation(value, pytype=(int, str)) if not value: return - elif value.endswith('%'): + + if isinstance(value, str): + if not value.endswith('%'): + raise configexc.ValidationError(value, "does not end with %") + try: - intval = int(value[:-1]) + floatval = float(value[:-1]) except ValueError: raise configexc.ValidationError(value, "invalid percentage!") - if self.minperc is not None and intval < self.minperc: + + if self.minperc is not None and floatval < self.minperc: raise configexc.ValidationError(value, "must be {}% or " "more!".format(self.minperc)) - if self.maxperc is not None and intval > self.maxperc: + if self.maxperc is not None and floatval > self.maxperc: raise configexc.ValidationError(value, "must be {}% or " "less!".format(self.maxperc)) + + # Note we don't actually return the integer here, as we need to know + # whether it was a percentage. else: - try: - intval = int(value) - except ValueError: - raise configexc.ValidationError(value, "must be integer or " - "percentage!") - if self.minint is not None and intval < self.minint: - raise configexc.ValidationError(value, "must be {} or " - "bigger!".format(self.minint)) - if self.maxint is not None and intval > self.maxint: - raise configexc.ValidationError(value, "must be {} or " - "smaller!".format(self.maxint)) + self._validate_bounds(value) + return value class Command(BaseType): """Base class for a command value with arguments.""" - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + # FIXME:conf require a list here? + self._basic_validation(value, pytype=str) if not value: return split = value.split() if not split or split[0] not in cmdutils.cmd_dict: raise configexc.ValidationError(value, "must be a valid command!") + return value def complete(self): out = [] @@ -711,57 +657,35 @@ class QtColor(BaseType): """Base class for QColor.""" - def validate(self, value): - self._basic_validation(value) - if not value: - return - elif QColor.isValidColor(value): - pass - else: - raise configexc.ValidationError(value, "must be a valid color") - - def transform(self, value): + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: return None - else: - return QColor(value) - -class CssColor(BaseType): - - """Base class for a CSS color value.""" - - def validate(self, value): - self._basic_validation(value) - if not value: - return - elif value.startswith('-'): - # custom function name, won't validate. - pass - elif QColor.isValidColor(value): - pass + color = QColor(value) + if color.isValid(): + return color else: raise configexc.ValidationError(value, "must be a valid color") -class QssColor(CssColor): +class QssColor(QtColor): """Color used in a Qt stylesheet.""" - def validate(self, value): + def from_py(self, value): functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient', 'qradialgradient', 'qconicalgradient'] - self._basic_validation(value) + self._basic_validation(value, pytype=str) if not value: - return - elif (any(value.startswith(func + '(') for func in functions) and - value.endswith(')')): + return None + + if (any(value.startswith(func + '(') for func in functions) and + value.endswith(')')): # QColor doesn't handle these - pass - elif QColor.isValidColor(value): - pass - else: - raise configexc.ValidationError(value, "must be a valid color") + return value + + return super().from_py(value) class Font(BaseType): @@ -784,38 +708,46 @@ class Font(BaseType): )* # 0-inf size/weight/style tags (?P.+)$ # mandatory font family""", re.VERBOSE) - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: - return - elif not self.font_regex.match(value): # pragma: no cover + return None + + if not self.font_regex.match(value): # FIXME:conf this used to have "pragma: no cover" raise configexc.ValidationError(value, "must be a valid font") + return value + class FontFamily(Font): """A Qt font family.""" - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: - return + return None + match = self.font_regex.match(value) - if not match: # pragma: no cover + if not match: # FIXME:conf this used to have "pragma: no cover" raise configexc.ValidationError(value, "must be a valid font") for group in 'style', 'weight', 'namedweight', 'size': if match.group(group): raise configexc.ValidationError(value, "may not include a " "{}!".format(group)) + return value + class QtFont(Font): """A Font which gets converted to a QFont.""" - def transform(self, value): + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: return None + style_map = { 'normal': QFont.StyleNormal, 'italic': QFont.StyleItalic, @@ -830,6 +762,9 @@ class QtFont(Font): font.setWeight(QFont.Normal) match = self.font_regex.match(value) + if not match: # FIXME:conf this used to have "pragma: no cover" + raise configexc.ValidationError(value, "must be a valid font") + style = match.group('style') weight = match.group('weight') namedweight = match.group('namedweight') @@ -869,17 +804,120 @@ class Regex(BaseType): super().__init__(none_ok) self.flags = flags - def validate(self, value): - self._basic_validation(value) - if not value: - return - _validate_regex(value, self.flags) + def _compile_regex(self, pattern): + """Check if the given regex is valid. - def transform(self, value): + This is more complicated than it could be since there's a warning on + invalid escapes with newer Python versions, and we want to catch that + case and treat it as invalid. + """ + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter('always') + try: + compiled = re.compile(pattern, self.flags) + except re.error as e: + raise configexc.ValidationError( + pattern, "must be a valid regex - " + str(e)) + except RuntimeError: # pragma: no cover + raise configexc.ValidationError( + pattern, "must be a valid regex - recursion depth " + "exceeded") + + for w in recorded_warnings: + if (issubclass(w.category, DeprecationWarning) and + str(w.message).startswith('bad escape')): + raise configexc.ValidationError( + pattern, "must be a valid regex - " + str(w.message)) + else: + warnings.warn(w.message) + + return compiled + + def from_py(self, value): + regex_type = type(re.compile('')) + self._basic_validation(value, pytype=(str, regex_type)) if not value: return None + elif isinstance(value, str): + return self._compile_regex(value) else: - return re.compile(value, self.flags) + # FIXME:conf is it okay if we ignore flags here? + return value + + +class Dict(BaseType): + + """A JSON-like dictionary for custom HTTP headers.""" + + def __init__(self, keytype, valtype, *, encoding=None, fixed_keys=None, + none_ok=False): + super().__init__(none_ok) + self.keytype = keytype + self.valtype = valtype + self.encoding = encoding + self.fixed_keys = fixed_keys + + def _validate_encoding(self, value, what): + """Check if the given value fits into the configured encoding. + + Raises ValidationError if not. + + Args: + value: The value to check. + what: Either 'key' or 'value'. + """ + if self.encoding is None: + return + # Should be checked by keytype/valtype already. + assert isinstance(value, str), value + + try: + value.encode(self.encoding) + except UnicodeEncodeError as e: + msg = "{} {!r} contains non-{} characters: {}".format( + what.capitalize(), value, self.encoding, e) + raise configexc.ValidationError(value, msg) + + def _validate_keys(self, value): + if (self.fixed_keys is not None and + value.keys() != set(self.fixed_keys)): + raise configexc.ValidationError( + value, "Expected keys {}".format(self.fixed_keys)) + + def from_str(self, value): + try: + json_val = json.loads(value) + except ValueError as e: + raise configexc.ValidationError(value, str(e)) + + # Can't use self.from_py here because we don't want to call from_py on + # the values. + self._basic_validation(json_val, pytype=dict) + if not json_val: + return None + + self._validate_keys(json_val) + converted = {} + for key, val in json_val.items(): + self._validate_encoding(key, 'key') + self._validate_encoding(val, 'value') + converted[self.keytype.from_str(key)] = self.valtype.from_str(val) + + return converted + + def from_py(self, value): + self._basic_validation(value, pytype=dict) + if not value: + return None + + self._validate_keys(value) + converted = {} + for key, val in value.items(): + self._validate_encoding(key, 'key') + self._validate_encoding(val, 'value') + converted[self.keytype.from_py(key)] = self.valtype.from_py(val) + + return converted class File(BaseType): @@ -890,19 +928,11 @@ class File(BaseType): super().__init__(**kwargs) self.required = required - def transform(self, value): + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: return None - value = os.path.expanduser(value) - value = os.path.expandvars(value) - if not os.path.isabs(value): - value = os.path.join(standarddir.config(), value) - return value - def validate(self, value): - self._basic_validation(value) - if not value: - return value = os.path.expanduser(value) value = os.path.expandvars(value) try: @@ -917,12 +947,14 @@ class File(BaseType): except UnicodeEncodeError as e: raise configexc.ValidationError(value, e) + return value + class Directory(BaseType): """A directory on the local filesystem.""" - def validate(self, value): + def from_py(self, value): self._basic_validation(value) if not value: return @@ -938,11 +970,7 @@ class Directory(BaseType): except UnicodeEncodeError as e: raise configexc.ValidationError(value, e) - def transform(self, value): - if not value: - return None - value = os.path.expandvars(value) - return os.path.expanduser(value) + return value class FormatString(BaseType): @@ -953,19 +981,21 @@ class FormatString(BaseType): super().__init__(none_ok) self.fields = fields - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: - return - s = self.transform(value) + return None + try: - return s.format(**{k: '' for k in self.fields}) + return value.format(**{k: '' for k in self.fields}) except (KeyError, IndexError) as e: raise configexc.ValidationError(value, "Invalid placeholder " "{}".format(e)) except ValueError as e: raise configexc.ValidationError(value, str(e)) + return value + class ShellCommand(BaseType): @@ -979,23 +1009,22 @@ class ShellCommand(BaseType): super().__init__(none_ok) self.placeholder = placeholder - def validate(self, value): - self._basic_validation(value) - if not value: - return + def from_str(self, value): try: - shlex.split(value) + return self.from_py(shlex.split(value)) except ValueError as e: raise configexc.ValidationError(value, str(e)) - if self.placeholder and '{}' not in value: - raise configexc.ValidationError(value, "needs to contain a " - "{}-placeholder.") - def transform(self, value): + def from_py(self, value): + # FIXME:conf require a str/list here? + self._basic_validation(value, pytype=list) if not value: return None - else: - return shlex.split(value) + + if self.placeholder and '{}' not in ' '.join(value): + raise configexc.ValidationError(value, "needs to contain a " + "{}-placeholder.") + return value class Proxy(BaseType): @@ -1008,16 +1037,24 @@ class Proxy(BaseType): ('system', "Use the system wide proxy."), ('none', "Don't use any proxy")) - def validate(self, value): + def from_py(self, value): from qutebrowser.utils import urlutils - self._basic_validation(value) + self._basic_validation(value, pytype=str) if not value: - return - elif value in self.valid_values: - return + return None try: - self.transform(value) + if value == 'system': + return SYSTEM_PROXY + + if value == 'none': + url = QUrl('direct://') + else: + # If we add a special value to valid_values, we need to handle + # it here! + assert value not in self.valid_values, value + url = QUrl(value) + return urlutils.proxy_from_url(url) except (urlutils.InvalidUrlError, urlutils.InvalidProxyTypeError) as e: raise configexc.ValidationError(value, e) @@ -1032,38 +1069,19 @@ class Proxy(BaseType): out.append(('pac+https://example.com/proxy.pac', 'Proxy autoconfiguration file URL')) return out - def transform(self, value): - from qutebrowser.utils import urlutils - if not value: - return None - elif value == 'system': - return SYSTEM_PROXY - - if value == 'none': - url = QUrl('direct://') - else: - url = QUrl(value) - return urlutils.proxy_from_url(url) - - -class SearchEngineName(BaseType): - - """A search engine name.""" - - def validate(self, value): - self._basic_validation(value) - class SearchEngineUrl(BaseType): """A search engine URL.""" - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: - return - elif not ('{}' in value or '{0}' in value): + return None + + if not ('{}' in value or '{0}' in value): raise configexc.ValidationError(value, "must contain \"{}\"") + try: value.format("") except (KeyError, IndexError) as e: @@ -1077,63 +1095,68 @@ class SearchEngineUrl(BaseType): raise configexc.ValidationError( value, "invalid url, {}".format(url.errorString())) + return value + class FuzzyUrl(BaseType): """A single URL.""" - def validate(self, value): - self._basic_validation(value) - if not value: - return - from qutebrowser.utils import urlutils - try: - self.transform(value) - except urlutils.InvalidUrlError as e: - raise configexc.ValidationError(value, str(e)) - - def transform(self, value): + def from_py(self, value): from qutebrowser.utils import urlutils + self._basic_validation(value, pytype=str) if not value: return None - else: + + try: return urlutils.fuzzy_url(value, do_search=False) + except urlutils.InvalidUrlError as e: + raise configexc.ValidationError(value, str(e)) PaddingValues = collections.namedtuple('PaddingValues', ['top', 'bottom', 'left', 'right']) -class Padding(List): +class Padding(Dict): """Setting for paddings around elements.""" _show_valtype = False def __init__(self, none_ok=False, valid_values=None): - super().__init__(Int(minval=0, none_ok=none_ok), + super().__init__(keytype=String(), valtype=Int(minval=0), + fixed_keys=['top', 'bottom', 'left', 'right'], none_ok=none_ok, length=4) - self.valtype.valid_values = valid_values + # FIXME:conf + assert valid_values is None, valid_values - def transform(self, value): - elems = super().transform(value) - if elems is None: - return elems - return PaddingValues(*elems) + def from_str(self, value): + d = super().from_str(value) + if not d: + return None + return PaddingValues(**d) + + def from_py(self, value): + d = super().from_py(value) + if not d: + return None + return PaddingValues(**d) class Encoding(BaseType): """Setting for a python encoding.""" - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: - return + return None try: codecs.lookup(value) except LookupError: raise configexc.ValidationError(value, "is not a valid encoding!") + return value class AutoSearch(BaseType): @@ -1148,14 +1171,22 @@ class AutoSearch(BaseType): ('dns', "Use DNS requests (might be slow!)."), ('false', "Never search automatically.")) - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=(str, bool)) if not value: - return + return None + + if isinstance(value, bool): + if self.booltype.from_py(value): + # boolean true is an alias for naive matching + return 'naive' + else: + return False elif value.lower() in ['naive', 'dns']: - pass + return value.lower() else: - self.booltype.validate(value) + # Should be prevented by valid_values + assert False, value def transform(self, value): if not value: @@ -1163,10 +1194,7 @@ class AutoSearch(BaseType): elif value.lower() in ['naive', 'dns']: return value.lower() elif self.booltype.transform(value): - # boolean true is an alias for naive matching - return 'naive' - else: - return False + pass class Position(MappingType): @@ -1231,68 +1259,6 @@ class Url(BaseType): "{}".format(val.errorString())) -class Dict(BaseType): - - """A JSON-like dictionary for custom HTTP headers.""" - - # FIXME:conf validate correctly - - def __init__(self, keytype, valtype, none_ok=False): - super().__init__(none_ok) - self.keytype = keytype - self.valtype = valtype - - def _validate_str(self, value, what): - """Check if the given thing is an ascii-only string. - - Raises ValidationError if not. - - Args: - value: The value to check. - what: Either 'key' or 'value'. - """ - if not isinstance(value, str): - msg = "Expected string for {} {!r} but got {}".format( - what, value, type(value)) - raise configexc.ValidationError(value, msg) - - try: - value.encode('ascii') - except UnicodeEncodeError as e: - msg = "{} {!r} contains non-ascii characters: {}".format( - what.capitalize(), value, e) - raise configexc.ValidationError(value, msg) - - def validate(self, value): - self._basic_validation(value) - if not value: - return - - try: - json_val = json.loads(value) - except ValueError as e: - raise configexc.ValidationError(value, str(e)) - - if not isinstance(json_val, dict): - raise configexc.ValidationError(value, "Expected json dict, but " - "got {}".format(type(json_val))) - if not json_val: - if self.none_ok: - return - else: - raise configexc.ValidationError(value, "may not be empty!") - - for key, val in json_val.items(): - self._validate_str(key, 'key') - self._validate_str(val, 'value') - - def transform(self, value): - if not value: - return None - val = json.loads(value) - return val or None - - class SessionName(BaseType): """The name of a session.""" From 5ec47da12741c9f4f83bbd37ced3c331bcc61466 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 10:28:05 +0200 Subject: [PATCH 011/516] Get rid of configtypes.AutoSearch and IgnoreCase --- qutebrowser/browser/browsertab.py | 19 +++++- qutebrowser/browser/webengine/webenginetab.py | 20 +++--- qutebrowser/browser/webkit/webkittab.py | 23 +++---- qutebrowser/config/configdata.yml | 14 +++- qutebrowser/config/configtypes.py | 66 ------------------- qutebrowser/utils/urlutils.py | 6 +- tests/unit/utils/test_urlutils.py | 6 +- 7 files changed, 52 insertions(+), 102 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 7adb9608e..fd8f8872f 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -188,13 +188,28 @@ class AbstractSearch(QObject): self.text = None self.search_displayed = False - def search(self, text, *, ignore_case=False, reverse=False, + def _is_case_sensitive(self, ignore_case): + """Check if case-sensitivity should be used. + + This assumes self.text is already set properly. + + Arguments: + ignore_case: The ignore_case value from the config. + """ + mapping = { + 'smart': not self.text.islower(), + 'never': False, + 'always': True, + } + return mapping[ignore_case] + + def search(self, text, *, ignore_case='never', reverse=False, result_cb=None): """Find the given text on the page. Args: text: The text to search for. - ignore_case: Search case-insensitively. (True/False/'smart') + ignore_case: Search case-insensitively. ('always'/'never/'smart') reverse: Reverse search direction. result_cb: Called with a bool indicating whether a match was found. """ diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 0a4c2dfc7..1345e8001 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -152,20 +152,16 @@ class WebEngineSearch(browsertab.AbstractSearch): callback(found) self._widget.findText(text, flags, wrapped_callback) - def search(self, text, *, ignore_case=False, reverse=False, + def search(self, text, *, ignore_case='never', reverse=False, result_cb=None): - flags = QWebEnginePage.FindFlags(0) - if ignore_case == 'smart': - if not text.islower(): - flags |= QWebEnginePage.FindCaseSensitively - elif not ignore_case: - flags |= QWebEnginePage.FindCaseSensitively - if reverse: - flags |= QWebEnginePage.FindBackward - self.text = text - self._flags = flags - self._find(text, flags, result_cb, 'search') + self._flags = QWebEnginePage.FindFlags(0) + if self._is_case_sensitive(ignore_case): + self._flags |= QWebEnginePage.FindCaseSensitively + if reverse: + self._flags |= QWebEnginePage.FindBackward + + self._find(text, self._flags, result_cb, 'search') def clear(self): self.search_displayed = False diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index fa626e863..e901f3a46 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -140,24 +140,21 @@ class WebKitSearch(browsertab.AbstractSearch): self._widget.findText('') self._widget.findText('', QWebPage.HighlightAllOccurrences) - def search(self, text, *, ignore_case=False, reverse=False, + def search(self, text, *, ignore_case='never', reverse=False, result_cb=None): self.search_displayed = True - flags = QWebPage.FindWrapsAroundDocument - if ignore_case == 'smart': - if not text.islower(): - flags |= QWebPage.FindCaseSensitively - elif not ignore_case: - flags |= QWebPage.FindCaseSensitively + self.text = text + self._flags = QWebPage.FindWrapsAroundDocument + if self._is_case_sensitive(ignore_case): + self._flags |= QWebPage.FindCaseSensitively if reverse: - flags |= QWebPage.FindBackward + self._flags |= QWebPage.FindBackward # We actually search *twice* - once to highlight everything, then again # to get a mark so we can navigate. - found = self._widget.findText(text, flags) - self._widget.findText(text, flags | QWebPage.HighlightAllOccurrences) - self.text = text - self._flags = flags - self._call_cb(result_cb, found, text, flags, 'search') + found = self._widget.findText(text, self._flags) + self._widget.findText(text, + self._flags | QWebPage.HighlightAllOccurrences) + self._call_cb(result_cb, found, text, self._flags, 'search') def next_result(self, *, result_cb=None): self.search_displayed = True diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 93a802c19..51a3e8d7f 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1,7 +1,12 @@ # general ignore_case: - type: IgnoreCase + type: + name: String + valid_values: + - always: Search case-insensitively + - never: Search case-sensitively + - smart: Search case-sensitively if there are capital chars default: smart desc: Whether to find text on a page case-insensitively. @@ -45,7 +50,12 @@ default_page: for a blank page. auto_search: - type: AutoSearch + type: + name: String + valid_values: + - naive: Use simple/naive check. + - dns: Use DNS requests (might be slow!). + - never: Never search automatically. default: naive desc: Whether to start a search when something else than a URL is entered. diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index e0b1c7084..f5255048c 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1159,44 +1159,6 @@ class Encoding(BaseType): return value -class AutoSearch(BaseType): - - """Whether to start a search when something else than a URL is entered.""" - - def __init__(self, none_ok=False): - super().__init__(none_ok) - self.booltype = Bool(none_ok=none_ok) - self.valid_values = ValidValues( - ('naive', "Use simple/naive check."), - ('dns', "Use DNS requests (might be slow!)."), - ('false', "Never search automatically.")) - - def from_py(self, value): - self._basic_validation(value, pytype=(str, bool)) - if not value: - return None - - if isinstance(value, bool): - if self.booltype.from_py(value): - # boolean true is an alias for naive matching - return 'naive' - else: - return False - elif value.lower() in ['naive', 'dns']: - return value.lower() - else: - # Should be prevented by valid_values - assert False, value - - def transform(self, value): - if not value: - return None - elif value.lower() in ['naive', 'dns']: - return value.lower() - elif self.booltype.transform(value): - pass - - class Position(MappingType): """The position of the tab bar.""" @@ -1337,34 +1299,6 @@ class NewTabPosition(BaseType): ('last', "At the end.")) -class IgnoreCase(Bool): - - """Whether to ignore case when searching.""" - - def __init__(self, none_ok=False): - super().__init__(none_ok) - self.valid_values = ValidValues( - ('true', "Search case-insensitively"), - ('false', "Search case-sensitively"), - ('smart', "Search case-sensitively if there " - "are capital chars")) - - def transform(self, value): - if value.lower() == 'smart': - return 'smart' - else: - return super().transform(value) - - def validate(self, value): - self._basic_validation(value) - if not value: - return - if value.lower() == 'smart': - return - else: - super().validate(value) - - class UserAgent(BaseType): """The user agent to use.""" diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 24bbfbc9c..5adc5d9c7 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -194,7 +194,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True, url = qurl_from_user_input(urlstr) log.url.debug("Converting fuzzy term {!r} to URL -> {}".format( urlstr, url.toDisplayString())) - if do_search and config.val.auto_search and urlstr: + if do_search and config.val.auto_search != 'never' and urlstr: qtutils.ensure_valid(url) else: if not url.isValid(): @@ -248,7 +248,7 @@ def is_url(urlstr): qurl = QUrl(urlstr) qurl_userinput = qurl_from_user_input(urlstr) - if not autosearch: + if autosearch == 'never': # no autosearch, so everything is a URL unless it has an explicit # search engine. try: @@ -270,7 +270,7 @@ def is_url(urlstr): log.url.debug("Is localhost.") url = True elif is_special_url(qurl): - # Special URLs are always URLs, even with autosearch=False + # Special URLs are always URLs, even with autosearch=never log.url.debug("Is a special URL.") url = True elif autosearch == 'dns': diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 10bd60bed..30de96a39 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -347,7 +347,7 @@ def test_get_search_url_invalid(urlutils_config_stub, url): # autosearch = False (False, True, False, 'This is a URL without autosearch'), ]) -@pytest.mark.parametrize('auto_search', ['dns', 'naive', False]) +@pytest.mark.parametrize('auto_search', ['dns', 'naive', 'never']) def test_is_url(urlutils_config_stub, fake_dns, is_url, is_url_no_autosearch, uses_dns, url, auto_search): """Test is_url(). @@ -384,11 +384,9 @@ def test_is_url(urlutils_config_stub, fake_dns, is_url, is_url_no_autosearch, assert not fake_dns.used assert result == is_url elif auto_search == 'naive': - urlutils_config_stub.data['general']['auto-search'] = 'naive' assert urlutils.is_url(url) == is_url assert not fake_dns.used - elif not auto_search: - urlutils_config_stub.data['general']['auto-search'] = False + elif auto_search == 'never': assert urlutils.is_url(url) == is_url_no_autosearch assert not fake_dns.used else: From 616aad84d81db68ddf5da6a2d024b4a5c940604c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 10:40:05 +0200 Subject: [PATCH 012/516] More configtypes refactoring --- qutebrowser/config/configdata.yml | 9 +- qutebrowser/config/configtypes.py | 137 ++++++++++++++++-------------- 2 files changed, 78 insertions(+), 68 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 51a3e8d7f..ebe2cefba 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -462,9 +462,12 @@ content.custom_headers: default: {} type: name: Dict - keytype: String - valtype: String - encoding: ascii + keytype: + - name: String + - encoding: ascii + valtype: + - name: String + - encoding: ascii none_ok: true desc: Set custom headers for qutebrowser HTTP requests. diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index f5255048c..fae84de70 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -255,10 +255,10 @@ class String(BaseType): minlen: Minimum length (inclusive). maxlen: Maximum length (inclusive). forbidden: Forbidden chars in the string. - _completions: completions to be used, or None + completions: completions to be used, or None """ - def __init__(self, minlen=None, maxlen=None, forbidden=None, + def __init__(self, minlen=None, maxlen=None, forbidden=None, encoding=None, none_ok=False, completions=None, valid_values=None): super().__init__(none_ok) self.valid_values = valid_values @@ -274,6 +274,25 @@ class String(BaseType): self.maxlen = maxlen self.forbidden = forbidden self._completions = completions + self.encoding = encoding + + def _validate_encoding(self, value): + """Check if the given value fits into the configured encoding. + + Raises ValidationError if not. + + Args: + value: The value to check. + """ + if self.encoding is None: + return + + try: + value.encode(self.encoding) + except UnicodeEncodeError as e: + msg = "{!r} contains non-{} characters: {}".format( + value, self.encoding, e) + raise configexc.ValidationError(value, msg) def from_py(self, value): self._basic_validation(value, pytype=str) @@ -380,12 +399,19 @@ class FlagList(List): super().__init__(BaseType(), none_ok) self.valtype.valid_values = valid_values + def _check_duplicates(self, values): + if len(set(values)) != len(values): + raise configexc.ValidationError( + values, "List contains duplicate values!") + + def from_str(self, value): + vals = super().from_str(value) + self._check_duplicates(vals) + return vals + def from_py(self, value): vals = super().from_py(value) - # Check for duplicate values - if len(set(vals)) != len(vals): - raise configexc.ValidationError( - value, "List contains duplicate values!") + self._check_duplicates(vals) return vals def complete(self): @@ -849,35 +875,12 @@ class Dict(BaseType): """A JSON-like dictionary for custom HTTP headers.""" - def __init__(self, keytype, valtype, *, encoding=None, fixed_keys=None, - none_ok=False): + def __init__(self, keytype, valtype, *, fixed_keys=None, none_ok=False): super().__init__(none_ok) self.keytype = keytype self.valtype = valtype - self.encoding = encoding self.fixed_keys = fixed_keys - def _validate_encoding(self, value, what): - """Check if the given value fits into the configured encoding. - - Raises ValidationError if not. - - Args: - value: The value to check. - what: Either 'key' or 'value'. - """ - if self.encoding is None: - return - # Should be checked by keytype/valtype already. - assert isinstance(value, str), value - - try: - value.encode(self.encoding) - except UnicodeEncodeError as e: - msg = "{} {!r} contains non-{} characters: {}".format( - what.capitalize(), value, self.encoding, e) - raise configexc.ValidationError(value, msg) - def _validate_keys(self, value): if (self.fixed_keys is not None and value.keys() != set(self.fixed_keys)): @@ -897,13 +900,9 @@ class Dict(BaseType): return None self._validate_keys(json_val) - converted = {} - for key, val in json_val.items(): - self._validate_encoding(key, 'key') - self._validate_encoding(val, 'value') - converted[self.keytype.from_str(key)] = self.valtype.from_str(val) - return converted + return {self.keytype.from_str(key): self.valtype.from_str(val) + for key, val in json_val.items()} def from_py(self, value): self._basic_validation(value, pytype=dict) @@ -911,13 +910,9 @@ class Dict(BaseType): return None self._validate_keys(value) - converted = {} - for key, val in value.items(): - self._validate_encoding(key, 'key') - self._validate_encoding(val, 'value') - converted[self.keytype.from_py(key)] = self.valtype.from_py(val) - return converted + return {self.keytype.from_py(key): self.valtype.from_py(val) + for key, val in value.items()} class File(BaseType): @@ -1205,30 +1200,27 @@ class Url(BaseType): """A URL.""" - def transform(self, value): + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: return None - else: - return QUrl.fromUserInput(value) - def validate(self, value): - self._basic_validation(value) - if not value: - return - val = self.transform(value) - if not val.isValid(): + qurl = QUrl.fromUserInput(value) + if not qurl.isValid(): raise configexc.ValidationError(value, "invalid URL - " - "{}".format(val.errorString())) + "{}".format(qurl.errorString())) + return qurl class SessionName(BaseType): """The name of a session.""" - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=str) if value.startswith('_'): raise configexc.ValidationError(value, "may not start with '_'!") + return value class SelectOnRemove(MappingType): @@ -1270,20 +1262,30 @@ class ConfirmQuit(FlagList): "downloads are running"), ('never', "Never show a confirmation.")) - def validate(self, value): - super().validate(value) - if not value: - return - values = [x for x in self.transform(value) if x] - + def _check_values(self, values): + """Check whether the values can be combined in the way they are.""" # Never can't be set with other options if 'never' in values and len(values) > 1: raise configexc.ValidationError( - value, "List cannot contain never!") + values, "List cannot contain never!") # Always can't be set with other options elif 'always' in values and len(values) > 1: raise configexc.ValidationError( - value, "List cannot contain always!") + values, "List cannot contain always!") + + def from_py(self, value): + values = super().from_py(value) + if not values: + return None + self._check_values(values) + return values + + def from_str(self, value): + values = super().from_str(value) + if not values: + return None + self._check_values(values) + return values class NewTabPosition(BaseType): @@ -1303,6 +1305,8 @@ class UserAgent(BaseType): """The user agent to use.""" + # FIXME:conf refactor + def validate(self, value): self._basic_validation(value) try: @@ -1375,10 +1379,11 @@ class TimestampTemplate(BaseType): for reference. """ - def validate(self, value): - self._basic_validation(value) + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: - return + return None + try: # Dummy check to see if the template is valid datetime.datetime.now().strftime(value) @@ -1386,3 +1391,5 @@ class TimestampTemplate(BaseType): # thrown on invalid template string raise configexc.ValidationError( value, "Invalid format string: {}".format(error)) + + return value From 8de0445661a75b3f6586720976b7fa95a55376af Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 11:29:21 +0200 Subject: [PATCH 013/516] Move UserAgent to configdata --- qutebrowser/config/configdata.yml | 46 ++++++++++++++++++++- qutebrowser/config/configtypes.py | 69 ------------------------------- scripts/dev/ua_fetch.py | 25 ++++------- 3 files changed, 54 insertions(+), 86 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index ebe2cefba..06a6aef1b 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -421,8 +421,52 @@ content.referer_header: content.user_agent: default: "" type: - name: UserAgent + name: String none_ok: true + completions: + # To update the following list of user agents, run the script 'ua_fetch.py' + # Vim-protip: Place your cursor below this comment and run + # :r!python scripts/dev/ua_fetch.py + - - "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:53.0) Gecko/20100101 + Firefox/53.0" + - Firefox 53.0 Win8.1 + - - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 + Firefox/53.0" + - Firefox 53.0 Linux + - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 + Firefox/53.0" + - Firefox 53.0 MacOSX + + - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 + (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4" + - Safari Generic MacOSX + - - "Mozilla/5.0 (iPad; CPU OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 + (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1" + - Mobile Safari 10.0 iOS + + - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, + like Gecko) Chrome/58.0.3029.110 Safari/537.36" + - Chrome Generic Win10 + - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" + - Chrome Generic MacOSX + - - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like + Gecko) Chrome/58.0.3029.110 Safari/537.36" + - Chrome Generic Linux + + - - "Mozilla/5.0 (compatible; Googlebot/2.1; + +http://www.google.com/bot.html" + - Google Bot + - - "Wget/1.16.1 (linux-gnu)" + - wget 1.16.1 + - - "curl/7.40.0" + - curl 7.40.0 + - - "Mozilla/5.0 (Linux; U; Android 7.1.2) AppleWebKit/534.30 (KHTML, like + Gecko) Version/4.0 Mobile Safari/534.30" + - Mobile Generic Android + - - "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko" + - IE 11.0 for Desktop Win7 64-bit + desc: User agent to send. Empty to send the default. content.proxy: diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index fae84de70..dd2270926 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1301,75 +1301,6 @@ class NewTabPosition(BaseType): ('last', "At the end.")) -class UserAgent(BaseType): - - """The user agent to use.""" - - # FIXME:conf refactor - - def validate(self, value): - self._basic_validation(value) - try: - value.encode('ascii') - except UnicodeEncodeError as e: - msg = "User-Agent contains non-ascii characters: {}".format(e) - raise configexc.ValidationError(value, msg) - - # To update the following list of user agents, run the script 'ua_fetch.py' - # Vim-protip: Place your cursor below this comment and run - # :r!python scripts/dev/ua_fetch.py - def complete(self): - """Complete a list of common user agents.""" - out = [ - ('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 ' - 'Firefox/47.0', - "Firefox Generic Win7"), - ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) ' - 'Gecko/20100101 Firefox/47.0', - "Firefox Generic MacOSX"), - ('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 ' - 'Firefox/47.0', - "Firefox Generic Linux"), - - ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) ' - 'AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 ' - 'Safari/601.7.7', - "Safari Generic MacOSX"), - ('Mozilla/5.0 (iPad; CPU OS 9_3_2 like Mac OS X) ' - 'AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 ' - 'Mobile/13F69 Safari/601.1', - "Mobile Safari 9.0 iOS"), - - ('Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 ' - '(KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36', - "Chrome Generic Win10"), - ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) ' - 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 ' - 'Safari/537.36', - "Chrome Generic MacOSX"), - ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' - '(KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36', - "Chrome Generic Linux"), - - ('Mozilla/5.0 (compatible; Googlebot/2.1; ' - '+http://www.google.com/bot.html', - "Google Bot"), - ('Wget/1.16.1 (linux-gnu)', - "wget 1.16.1"), - ('curl/7.40.0', - "curl 7.40.0"), - - ('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like ' - 'Gecko', - "IE 11.0 for Desktop Win7 64-bit"), - - ('Mozilla/5.0 (Linux; U; Android 7.1.2) AppleWebKit/534.30 ' - '(KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', - "Mobile Generic Android") - ] - return out - - class TimestampTemplate(BaseType): """An strftime-like template for timestamps. diff --git a/scripts/dev/ua_fetch.py b/scripts/dev/ua_fetch.py index 7c39ad596..b276eff27 100755 --- a/scripts/dev/ua_fetch.py +++ b/scripts/dev/ua_fetch.py @@ -24,7 +24,7 @@ """Fetch list of popular user-agents. The script is based on a gist posted by github.com/averrin, the output of this -script is formatted to be pasted into configtypes.py. +script is formatted to be pasted into configdata.yml """ import requests @@ -90,7 +90,10 @@ def add_diversity(table): "curl 7.40.0"), ('Mozilla/5.0 (Linux; U; Android 7.1.2) AppleWebKit/534.30 ' '(KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', - "Mobile Generic Android") + "Mobile Generic Android"), + ('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like ' + 'Gecko', + "IE 11.0 for Desktop Win7 64-bit"), ] return table @@ -106,24 +109,14 @@ def main(): filtered = filter_list(fetched, lut) filtered = add_diversity(filtered) - tab = " " - print(tab + "def complete(self):") - print((2 * tab) + "\"\"\"Complete a list of common user agents.\"\"\"") - print((2 * tab) + "out = [") - + tab = " " for browser in ["Firefox", "Safari", "Chrome", "Obscure"]: for it in filtered[browser]: - print("{}(\'{}\',\n{} \"{}\"),".format(3 * tab, it[0], - 3 * tab, it[1])) + print('{}- - "{}"'.format(3 * tab, it[0])) + desc = it[1].replace('\xa0', ' ').replace(' ', ' ') + print("{}- {}".format(4 * tab, desc)) print("") - print("""\ - ('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like ' - 'Gecko', - "IE 11.0 for Desktop Win7 64-bit")""") - - print("{}]\n{}return out\n".format(2 * tab, 2 * tab)) - if __name__ == '__main__': main() From 3009e5eebebb3759267a72876c86788d064d6ff6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 11:31:43 +0200 Subject: [PATCH 014/516] Fix configtypes mistakes --- qutebrowser/config/configdata.yml | 8 ++++---- qutebrowser/config/configtypes.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 06a6aef1b..73c101c76 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -507,11 +507,11 @@ content.custom_headers: type: name: Dict keytype: - - name: String - - encoding: ascii + name: String + encoding: ascii valtype: - - name: String - - encoding: ascii + name: String + encoding: ascii none_ok: true desc: Set custom headers for qutebrowser HTTP requests. diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index dd2270926..1969ec946 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1122,7 +1122,7 @@ class Padding(Dict): def __init__(self, none_ok=False, valid_values=None): super().__init__(keytype=String(), valtype=Int(minval=0), fixed_keys=['top', 'bottom', 'left', 'right'], - none_ok=none_ok, length=4) + none_ok=none_ok) # FIXME:conf assert valid_values is None, valid_values From b5110b07f0e06efed4fa2f1807fca5c196105c22 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 11:34:37 +0200 Subject: [PATCH 015/516] Fix newconfig init --- qutebrowser/config/config.py | 8 +------- qutebrowser/config/newconfig.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index f52578040..36b64dd29 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -248,12 +248,6 @@ def _init_misc(): QSettings.setPath(fmt, QSettings.UserScope, path) -def _init_new_config(parent): - new_config = newconfig.NewConfigManager(parent) - new_config.read_defaults() - objreg.register('config', new_config) - - def init(parent=None): """Initialize the config. @@ -263,7 +257,7 @@ def init(parent=None): global val # _init_main_config(parent) configdata.init() - _init_new_config(parent) + newconfig.init(parent) val = newconfig.val _init_key_config(parent) _init_misc() diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index 6dabf79e1..caf8594ea 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -24,7 +24,7 @@ from PyQt5.QtCore import pyqtSignal, QObject from qutebrowser.config import configdata -from qutebrowser.utils import utils +from qutebrowser.utils import utils, objreg # An easy way to access the config from other code via config.val.foo val = None @@ -66,7 +66,7 @@ class NewConfigManager(QObject): val = self._values[option] except KeyError as e: raise UnknownOptionError(e) - return val.typ.transform(val.default) + return val.typ.from_py(val.default) def is_valid_prefix(self, prefix): """Check whether the given prefix is a valid prefix for some option.""" @@ -115,3 +115,11 @@ class ConfigContainer: return '{}.{}'.format(self._prefix, attr) else: return attr + + +def init(parent): + new_config = NewConfigManager(parent) + new_config.read_defaults() + objreg.register('config', new_config) + global val + val = ConfigContainer(new_config) From 5ab2c89a3779b0effa22baee83f971d95f62f325 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 12:07:00 +0200 Subject: [PATCH 016/516] Adjust config.change_filter --- qutebrowser/browser/adblock.py | 7 ++- qutebrowser/browser/webengine/webview.py | 2 +- qutebrowser/browser/webkit/cache.py | 4 +- qutebrowser/browser/webkit/cookies.py | 8 ++-- qutebrowser/browser/webkit/webview.py | 2 +- qutebrowser/completion/models/urlmodel.py | 2 +- qutebrowser/config/config.py | 54 ++++++++++------------- qutebrowser/config/configdata.py | 5 +++ qutebrowser/config/newconfig.py | 6 +-- qutebrowser/keyinput/modeman.py | 17 +++---- qutebrowser/mainwindow/messageview.py | 7 ++- qutebrowser/mainwindow/tabbedbrowser.py | 16 +++---- qutebrowser/mainwindow/tabwidget.py | 20 ++++----- qutebrowser/misc/consolewidget.py | 4 +- qutebrowser/misc/savemanager.py | 2 +- 15 files changed, 69 insertions(+), 87 deletions(-) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index b27a54424..93870a30a 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -180,7 +180,7 @@ class HostBlocker: self._config_blocked_hosts) self._blocked_hosts = set() self._done_count = 0 - urls = config.val.content.host_block_lists + urls = config.val.content.host_blocking.lists download_manager = objreg.get('qtnetwork-download-manager', scope='window', window='last-focused') if urls is None: @@ -292,11 +292,10 @@ class HostBlocker: message.info("adblock: Read {} hosts from {} sources.".format( len(self._blocked_hosts), self._done_count)) - @config.change_filter('content', 'host-block-lists') + @config.change_filter('content.host-blocking.lists') def on_config_changed(self): """Update files when the config changed.""" - urls = config.val.content.host_block_lists - if urls is None: + if config.val.content.host_blocking.lists is None: try: os.remove(self._local_hosts_file) except FileNotFoundError: diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index b650df5a7..f8cefc300 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -137,7 +137,7 @@ class WebEnginePage(QWebEnginePage): self._set_bg_color() objreg.get('config').changed.connect(self._set_bg_color) - @config.change_filter('colors', 'webpage.bg') + @config.change_filter('colors.webpage.bg') def _set_bg_color(self): col = config.val.colors.webpage.bg if col is None: diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 6f2478c37..2a916a0b4 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -42,10 +42,10 @@ class DiskCache(QNetworkDiskCache): maxsize=self.maximumCacheSize(), path=self.cacheDirectory()) - @config.change_filter('storage', 'cache-size') + @config.change_filter('content.cache_size') def _set_cache_size(self): """Set the cache size based on the config.""" - size = config.val.storage.cache_size + size = config.val.content.cache_size if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py index 2c9072f7d..9f2eb8d5e 100644 --- a/qutebrowser/browser/webkit/cookies.py +++ b/qutebrowser/browser/webkit/cookies.py @@ -50,7 +50,7 @@ class RAMCookieJar(QNetworkCookieJar): Return: True if one or more cookies are set for 'url', otherwise False. """ - if config.val.content.cookies_accept == 'never': + if config.val.content.cookies.accept == 'never': return False else: self.changed.emit() @@ -77,7 +77,7 @@ class CookieJar(RAMCookieJar): objreg.get('config').changed.connect(self.cookies_store_changed) objreg.get('save-manager').add_saveable( 'cookies', self.save, self.changed, - config_opt='content.cookies_store') + config_opt='content.cookies.store') def parse_cookies(self): """Parse cookies from lineparser and store them.""" @@ -105,10 +105,10 @@ class CookieJar(RAMCookieJar): self._lineparser.data = lines self._lineparser.save() - @config.change_filter('content', 'cookies-store') + @config.change_filter('content.cookies.store') def cookies_store_changed(self): """Delete stored cookies if cookies-store changed.""" - if not config.val.content.cookies_store: + if not config.val.content.cookies.store: self._lineparser.data = [] self._lineparser.save() self.changed.emit() diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 3daafd8ec..cbdfb1854 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -107,7 +107,7 @@ class WebView(QWebView): # deleted pass - @config.change_filter('colors', 'webpage.bg') + @config.change_filter('colors.webpage.bg') def _set_bg_color(self): """Set the webpage background color as configured.""" col = config.val.colors.webpage.bg diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index b52b50ff1..5409e651b 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -107,7 +107,7 @@ class UrlCompletionModel(base.BaseCompletionModel): self._history_cat.rowCount() > self._max_history): self._remove_oldest_history() - @config.change_filter('completion', 'timestamp-format') + @config.change_filter('completion.timestamp_format') def reformat_timestamps(self): """Reformat the timestamps if the config option was changed.""" for i in range(self._history_cat.rowCount()): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 36b64dd29..7a5dec930 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -62,30 +62,38 @@ class change_filter: # pylint: disable=invalid-name much cleaner to implement. Attributes: - _sectname: The section to be filtered. - _optname: The option to be filtered. + _option: An option or prefix to be filtered _function: Whether a function rather than a method is decorated. """ - def __init__(self, sectname, optname=None, function=False): + def __init__(self, option, function=False): """Save decorator arguments. Gets called on parse-time with the decorator arguments. Args: - sectname: The section to be filtered. - optname: The option to be filtered. + option: The option to be filtered. function: Whether a function rather than a method is decorated. """ - # FIXME:conf - # if sectname not in configdata.DATA: - # raise configexc.NoSectionError(sectname) - # if optname is not None and optname not in configdata.DATA[sectname]: - # raise configexc.NoOptionError(optname, sectname) - self._sectname = sectname - self._optname = optname + if (option not in configdata.DATA and + not configdata.is_valid_prefix(option)): + raise configexc.NoOptionError(option) + self._option = option self._function = function + def _check_match(self, option): + """Check if the given option matches the filter.""" + if option is None: + # Called directly, not from a config change event. + return True + elif option == self._option: + return True + elif option.startswith(self._option + '.'): + # prefix match + return True + else: + return False + def __call__(self, func): """Filter calls to the decorated function. @@ -104,27 +112,13 @@ class change_filter: # pylint: disable=invalid-name """ if self._function: @functools.wraps(func) - def wrapper(sectname=None, optname=None): - if sectname is None and optname is None: - # Called directly, not from a config change event. - return func() - elif sectname != self._sectname: - return - elif self._optname is not None and optname != self._optname: - return - else: + def wrapper(option=None): + if self._check_match(option): return func() else: @functools.wraps(func) - def wrapper(wrapper_self, sectname=None, optname=None): - if sectname is None and optname is None: - # Called directly, not from a config change event. - return func(wrapper_self) - elif sectname != self._sectname: - return - elif self._optname is not None and optname != self._optname: - return - else: + def wrapper(wrapper_self, option=None): + if self._check_match(option): return func(wrapper_self) return wrapper diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 510769dbb..bda605642 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -672,6 +672,11 @@ def _read_yaml(yaml_data): return parsed +def is_valid_prefix(prefix): + """Check whether the given prefix is a valid prefix for some option.""" + return any(key.startswith(prefix + '.') for key in DATA) + + def init(): """Initialize configdata from the YAML file.""" global DATA diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index caf8594ea..ae60e164c 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -68,10 +68,6 @@ class NewConfigManager(QObject): raise UnknownOptionError(e) return val.typ.from_py(val.default) - def is_valid_prefix(self, prefix): - """Check whether the given prefix is a valid prefix for some option.""" - return any(key.startswith(prefix + '.') for key in self._values) - class ConfigContainer: @@ -100,7 +96,7 @@ class ConfigContainer: options. """ name = self._join(attr) - if self._manager.is_valid_prefix(name): + if configdata.is_valid_prefix(name): return ConfigContainer(manager=self._manager, prefix=name) # If it's not a valid prefix, this will raise NoOptionError. self._manager.get(name) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 5ce55e670..44c157c6f 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -144,9 +144,6 @@ class ModeManager(QObject): self._parsers = {} self.mode = usertypes.KeyMode.normal self._releaseevents_to_pass = set() - self._forward_unbound_keys = config.get( - 'input', 'forward-unbound-keys') - objreg.get('config').changed.connect(self.set_forward_unbound_keys) def __repr__(self): return utils.get_repr(self, mode=self.mode) @@ -171,10 +168,12 @@ class ModeManager(QObject): event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or not event.text().strip()) + forward_unbound_keys = config.val.input.forward_unbound_keys + if handled: filter_this = True - elif (parser.passthrough or self._forward_unbound_keys == 'all' or - (self._forward_unbound_keys == 'auto' and is_non_alnum)): + elif (parser.passthrough or forward_unbound_keys == 'all' or + (forward_unbound_keys == 'auto' and is_non_alnum)): filter_this = False else: filter_this = True @@ -187,7 +186,7 @@ class ModeManager(QObject): log.modes.debug("handled: {}, forward-unbound-keys: {}, " "passthrough: {}, is_non_alnum: {} --> " "filter: {} (focused: {!r})".format( - handled, self._forward_unbound_keys, + handled, forward_unbound_keys, parser.passthrough, is_non_alnum, filter_this, focus_widget)) return filter_this @@ -313,12 +312,6 @@ class ModeManager(QObject): raise ValueError("Can't leave normal mode!") self.leave(self.mode, 'leave current') - @config.change_filter('input', 'forward-unbound-keys') - def set_forward_unbound_keys(self): - """Update local setting when config changed.""" - self._forward_unbound_keys = config.get( - 'input', 'forward-unbound-keys') - def eventFilter(self, event): """Filter all events based on the currently set mode. diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 92bdd17ac..ac29ce86b 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -95,12 +95,11 @@ class MessageView(QWidget): # The width isn't really relevant as we're expanding anyways. return QSize(-1, height) - @config.change_filter('ui', 'message-timeout') + @config.change_filter('messages.timeout') def _set_clear_timer_interval(self): """Configure self._clear_timer according to the config.""" - interval = config.val.ui.message_timeout - if interval != 0: - self._clear_timer.setInterval(interval) + if config.val.messages.timeout != 0: + self._clear_timer.setInterval(config.val.messages.timeout) @pyqtSlot() def clear_messages(self): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 377825d6f..4a1f5a921 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -159,7 +159,7 @@ class TabbedBrowser(tabwidget.TabWidget): widgets.append(widget) return widgets - @config.change_filter('ui', 'window-title-format') + @config.change_filter('window.title_format') def update_window_title(self): """Change the window title to match the current tab.""" idx = self.currentIndex() @@ -170,8 +170,8 @@ class TabbedBrowser(tabwidget.TabWidget): fields = self.get_tab_fields(idx) fields['id'] = self._win_id - fmt = config.val.ui.window_title_format - self.window().setWindowTitle(fmt.format(**fields)) + title = config.val.window.title_format.format(**fields) + self.window().setWindowTitle(title) def _connect_tab_signals(self, tab): """Set up the needed signals for tab.""" @@ -485,19 +485,17 @@ class TabbedBrowser(tabwidget.TabWidget): self._tab_insert_idx_right)) return idx - @config.change_filter('tabs', 'show-favicons') + @config.change_filter('tabs.favicons.show') def update_favicons(self): """Update favicons when config was changed.""" - show = config.val.tabs.show_favicons - tabs_are_wins = config.val.tabs.tabs_are_windows for i, tab in enumerate(self.widgets()): - if show: + if config.val.tabs.favicons.show: self.setTabIcon(i, tab.icon()) - if tabs_are_wins: + if config.val.tabs.tabs_are_windows: self.window().setWindowIcon(tab.icon()) else: self.setTabIcon(i, QIcon()) - if tabs_are_wins: + if config.val.tabs.tabs_are_windows: self.window().setWindowIcon(self.default_window_icon) @pyqtSlot() diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 6fdbea5dd..0dc5a8007 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -331,22 +331,20 @@ class TabBar(QTabBar): """Get the current tab object.""" return self.parent().currentWidget() - @config.change_filter('tabs', 'show') + @config.change_filter('tabs.show') def tabs_show(self): """Hide or show tab bar if needed when tabs->show got changed.""" self.maybe_hide() - @config.change_filter('tabs', 'show-switching-delay') + @config.change_filter('tabs.show_switching_delay') def on_show_switching_delay_changed(self): """Set timer interval when tabs->show-switching-delay got changed.""" - self._auto_hide_timer.setInterval( - config.val.tabs.show_switching_delay) + self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay) def on_current_changed(self): """Show tab bar when current tab got changed.""" self.maybe_hide() # for fullscreen tabs - show = config.val.tabs.show - if show == 'switching': + if config.val.tabs.show == 'switching': self.show() self._auto_hide_timer.start() @@ -408,24 +406,24 @@ class TabBar(QTabBar): # code sets layoutDirty so it actually relayouts the tabs. self.setIconSize(self.iconSize()) - @config.change_filter('fonts', 'tabbar') + @config.change_filter('fonts.tabbar') def set_font(self): """Set the tab bar font.""" self.setFont(config.val.fonts.tabbar) self.set_icon_size() - @config.change_filter('tabs', 'favicon-scale') + @config.change_filter('tabs.favicon.scale') def set_icon_size(self): """Set the tab bar favicon size.""" size = self.fontMetrics().height() - 2 - size *= config.val.tabs.favicon_scale + size *= config.val.tabs.favicon.scale self.setIconSize(QSize(size, size)) - @config.change_filter('colors', 'tabs.bg.bar') + @config.change_filter('colors.tabs.bar.bg') def set_colors(self): """Set the tab bar colors.""" p = self.palette() - p.setColor(QPalette.Window, config.val.colors.tabs.bg.bar) + p.setColor(QPalette.Window, config.val.colors.tabs.bar.bg) self.setPalette(p) @pyqtSlot(str, str) diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index f7e06c56b..8aab14704 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -102,7 +102,7 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): else: super().keyPressEvent(e) - @config.change_filter('fonts', 'debug-console') + @config.change_filter('fonts.debug_console') def update_font(self): """Set the correct font.""" self.setFont(config.val.fonts.debug_console) @@ -123,7 +123,7 @@ class ConsoleTextEdit(QTextEdit): def __repr__(self): return utils.get_repr(self) - @config.change_filter('fonts', 'debug-console') + @config.change_filter('fonts.debug_console') def update_font(self): """Update font when config changed.""" self.setFont(config.val.fonts.debug_console) diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index 57755b4cd..7b38f907f 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -126,7 +126,7 @@ class SaveManager(QObject): self.set_autosave_interval() objreg.get('config').changed.connect(self.set_autosave_interval) - @config.change_filter('general', 'auto-save-interval') + @config.change_filter('auto_save.interval') def set_autosave_interval(self): """Set the auto-save interval.""" interval = config.val.auto_save.interval From 3e3685b68bfcd1e21de69e067b5abf6b50d627ce Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 12:20:44 +0200 Subject: [PATCH 017/516] Initial configexc refactoring --- qutebrowser/browser/commands.py | 1 + qutebrowser/commands/runners.py | 4 +- qutebrowser/completion/completiondelegate.py | 13 ++- qutebrowser/config/config.py | 71 +-------------- qutebrowser/config/configexc.py | 29 +----- qutebrowser/config/newconfig.py | 92 ++++++++++++++++++-- qutebrowser/utils/urlutils.py | 6 +- tests/helpers/stubs.py | 2 + tests/unit/config/test_configexc.py | 18 +--- 9 files changed, 106 insertions(+), 130 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index c375188f4..185b7f5cd 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1541,6 +1541,7 @@ class CommandDispatcher: command)) path = 'commands.html#{}'.format(command) elif '->' in topic: + # FIXME:conf refactor parts = topic.split('->') if len(parts) != 2: raise cmdexc.CommandError("Invalid help topic {}!".format( diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index c9109ab78..79da78fbd 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -110,8 +110,8 @@ class CommandRunner(QObject): return default parts = text.strip().split(maxsplit=1) try: - alias = config.get('aliases', parts[0]) - except (configexc.NoOptionError, configexc.NoSectionError): + alias = config.val.aliases[parts[0]] + except KeyError: return default try: new_cmd = '{} {}'.format(alias, parts[1]) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index f17a77cbf..d0de544a7 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -147,16 +147,15 @@ class CompletionItemDelegate(QStyledItemDelegate): # We can't use drawContents because then the color would be ignored. clip = QRectF(0, 0, rect.width(), rect.height()) self._painter.save() + if self._opt.state & QStyle.State_Selected: - option = 'completion.item.selected.fg' + color = config.val.completion.item.selected.fg elif not self._opt.state & QStyle.State_Enabled: - option = 'completion.category.fg' + color = config.val.completion.category.fg else: - option = 'completion.fg' - try: - self._painter.setPen(config.get('colors', option)) - except configexc.NoOptionError: - self._painter.setPen(config.val.colors.completion.fg) + color = config.val.completion.fg + self._painter.setPen(color) + ctx = QAbstractTextDocumentLayout.PaintContext() ctx.palette.setColor(QPalette.Text, self._painter.pen().color()) if clip.isValid(): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 7a5dec930..f7b9cd93e 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -46,6 +46,9 @@ from qutebrowser.utils import (message, objreg, utils, standarddir, log, from qutebrowser.misc import objects from qutebrowser.utils.usertypes import Completion +# FIXME:conf compat +from qutebrowser.config.newconfig import change_filter + UNSET = object() @@ -54,74 +57,6 @@ UNSET = object() val = None -class change_filter: # pylint: disable=invalid-name - - """Decorator to filter calls based on a config section/option matching. - - This could also be a function, but as a class (with a "wrong" name) it's - much cleaner to implement. - - Attributes: - _option: An option or prefix to be filtered - _function: Whether a function rather than a method is decorated. - """ - - def __init__(self, option, function=False): - """Save decorator arguments. - - Gets called on parse-time with the decorator arguments. - - Args: - option: The option to be filtered. - function: Whether a function rather than a method is decorated. - """ - if (option not in configdata.DATA and - not configdata.is_valid_prefix(option)): - raise configexc.NoOptionError(option) - self._option = option - self._function = function - - def _check_match(self, option): - """Check if the given option matches the filter.""" - if option is None: - # Called directly, not from a config change event. - return True - elif option == self._option: - return True - elif option.startswith(self._option + '.'): - # prefix match - return True - else: - return False - - def __call__(self, func): - """Filter calls to the decorated function. - - Gets called when a function should be decorated. - - Adds a filter which returns if we're not interested in the change-event - and calls the wrapped function if we are. - - We assume the function passed doesn't take any parameters. - - Args: - func: The function to be decorated. - - Return: - The decorated function. - """ - if self._function: - @functools.wraps(func) - def wrapper(option=None): - if self._check_match(option): - return func() - else: - @functools.wraps(func) - def wrapper(wrapper_self, option=None): - if self._check_match(option): - return func(wrapper_self) - - return wrapper def get(*args, **kwargs): diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index b19d45d7b..c13d94d25 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -52,35 +52,10 @@ class ValidationError(Error): self.option = None -class NoSectionError(Error): - - """Raised when no section matches a requested option.""" - - def __init__(self, section): - super().__init__("Section {!r} does not exist!".format(section)) - self.section = section - - class NoOptionError(Error): """Raised when an option was not found.""" - def __init__(self, option, section): - super().__init__("No option {!r} in section {!r}".format( - option, section)) + def __init__(self, option): + super().__init__("No option {!r}".format(option)) self.option = option - self.section = section - - -class InterpolationSyntaxError(Error): - - """Raised when the source text contains invalid syntax. - - Current implementation raises this exception when the source text into - which substitutions are made does not conform to the required syntax. - """ - - def __init__(self, option, section, msg): - super().__init__(msg) - self.option = option - self.section = section diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index ae60e164c..6ceeb7b57 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -19,20 +19,17 @@ """New qutebrowser configuration code.""" +import functools from PyQt5.QtCore import pyqtSignal, QObject - -from qutebrowser.config import configdata +from qutebrowser.config import configdata, configexc from qutebrowser.utils import utils, objreg # An easy way to access the config from other code via config.val.foo val = None - -class UnknownOptionError(Exception): - - """Raised by NewConfigManager when an option is unknown.""" +_change_filters = [] class SectionStub: @@ -47,6 +44,83 @@ class SectionStub: return self._conf.get(self._name, item) +class change_filter: # pylint: disable=invalid-name + + """Decorator to filter calls based on a config section/option matching. + + This could also be a function, but as a class (with a "wrong" name) it's + much cleaner to implement. + + Attributes: + _option: An option or prefix to be filtered + _function: Whether a function rather than a method is decorated. + """ + + def __init__(self, option, function=False): + """Save decorator arguments. + + Gets called on parse-time with the decorator arguments. + + Args: + option: The option to be filtered. + function: Whether a function rather than a method is decorated. + """ + self._option = option + self._function = function + _change_filters.append(self) + + def validate(self): + """Make sure the configured option or prefix exists. + + We can't do this in __init__ as configdata isn't ready yet. + """ + if (self._option not in configdata.DATA and + not configdata.is_valid_prefix(self._option)): + raise configexc.NoOptionError(self._option) + + def _check_match(self, option): + """Check if the given option matches the filter.""" + if option is None: + # Called directly, not from a config change event. + return True + elif option == self._option: + return True + elif option.startswith(self._option + '.'): + # prefix match + return True + else: + return False + + def __call__(self, func): + """Filter calls to the decorated function. + + Gets called when a function should be decorated. + + Adds a filter which returns if we're not interested in the change-event + and calls the wrapped function if we are. + + We assume the function passed doesn't take any parameters. + + Args: + func: The function to be decorated. + + Return: + The decorated function. + """ + if self._function: + @functools.wraps(func) + def wrapper(option=None): + if self._check_match(option): + return func() + else: + @functools.wraps(func) + def wrapper(wrapper_self, option=None): + if self._check_match(option): + return func(wrapper_self) + + return wrapper + + class NewConfigManager(QObject): # FIXME:conf QObject? @@ -65,7 +139,7 @@ class NewConfigManager(QObject): try: val = self._values[option] except KeyError as e: - raise UnknownOptionError(e) + raise configexc.NoOptionError(e) return val.typ.from_py(val.default) @@ -117,5 +191,9 @@ def init(parent): new_config = NewConfigManager(parent) new_config.read_defaults() objreg.register('config', new_config) + global val val = ConfigContainer(new_config) + + for cf in _change_filters: + cf.validate() diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 5adc5d9c7..7ba457e2d 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -29,7 +29,7 @@ import urllib.parse from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy -from qutebrowser.config import config, configexc +from qutebrowser.config import config from qutebrowser.utils import log, qtutils, message, utils from qutebrowser.commands import cmdexc from qutebrowser.browser.network import pac @@ -70,8 +70,8 @@ def _parse_search_term(s): if len(split) == 2: engine = split[0] try: - config.get('searchengines', engine) - except configexc.NoOptionError: + config.val.searchengines[engine] + except KeyError: engine = None term = s else: diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 0551f5a86..dbab2e5f9 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -419,6 +419,8 @@ class ConfigStub(QObject): data: The config data to return. """ + # FIXME:conf refactor... + changed = pyqtSignal(str, str) def __init__(self, parent=None): diff --git a/tests/unit/config/test_configexc.py b/tests/unit/config/test_configexc.py index 330ad7b07..a6c02f72e 100644 --- a/tests/unit/config/test_configexc.py +++ b/tests/unit/config/test_configexc.py @@ -29,24 +29,10 @@ def test_validation_error(): assert str(e) == "Invalid value 'val' - msg" -def test_no_section_error(): - e = configexc.NoSectionError('sect') - assert e.section == 'sect' - assert str(e) == "Section 'sect' does not exist!" - - def test_no_option_error(): - e = configexc.NoOptionError('opt', 'sect') - assert e.section == 'sect' + e = configexc.NoOptionError('opt') assert e.option == 'opt' - assert str(e) == "No option 'opt' in section 'sect'" - - -def test_interpolation_syntax_error(): - e = configexc.InterpolationSyntaxError('opt', 'sect', 'msg') - assert e.section == 'sect' - assert e.option == 'opt' - assert str(e) == 'msg' + assert str(e) == "No option 'opt'" def test_backend_error(): From 1ed8df890326c94ed44610fe95546ee321716ba1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 12:22:04 +0200 Subject: [PATCH 018/516] Fix various typos/bugs --- qutebrowser/browser/adblock.py | 2 +- qutebrowser/config/configdata.yml | 2 +- qutebrowser/config/newconfig.py | 4 ++-- qutebrowser/mainwindow/tabwidget.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 93870a30a..cd4380fe3 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -292,7 +292,7 @@ class HostBlocker: message.info("adblock: Read {} hosts from {} sources.".format( len(self._blocked_hosts), self._done_count)) - @config.change_filter('content.host-blocking.lists') + @config.change_filter('content.host_blocking.lists') def on_config_changed(self): """Update files when the config changed.""" if config.val.content.host_blocking.lists is None: diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 73c101c76..02d819b9a 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1054,7 +1054,7 @@ content.cookies.accept: - never: "Don't accept cookies at all." desc: Control which cookies to accept. -cookies.cookies.store: +content.cookies.store: default: true type: Bool desc: Whether to store cookies. Note this option needs a restart with QtWebEngine diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index 6ceeb7b57..c10cee770 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -139,7 +139,7 @@ class NewConfigManager(QObject): try: val = self._values[option] except KeyError as e: - raise configexc.NoOptionError(e) + raise configexc.NoOptionError(option) return val.typ.from_py(val.default) @@ -173,7 +173,7 @@ class ConfigContainer: if configdata.is_valid_prefix(name): return ConfigContainer(manager=self._manager, prefix=name) # If it's not a valid prefix, this will raise NoOptionError. - self._manager.get(name) + return self._manager.get(name) def __setattr__(self, attr, value): if attr.startswith('_'): diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 0dc5a8007..9275eebbb 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -412,11 +412,11 @@ class TabBar(QTabBar): self.setFont(config.val.fonts.tabbar) self.set_icon_size() - @config.change_filter('tabs.favicon.scale') + @config.change_filter('tabs.favicons.scale') def set_icon_size(self): """Set the tab bar favicon size.""" size = self.fontMetrics().height() - 2 - size *= config.val.tabs.favicon.scale + size *= config.val.tabs.favicons.scale self.setIconSize(QSize(size, size)) @config.change_filter('colors.tabs.bar.bg') From 1a6511c7a832ba839ff01c615fc6b5f603470ed1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 12:37:32 +0200 Subject: [PATCH 019/516] Refactor most stuff using config.val.ui --- qutebrowser/app.py | 2 +- qutebrowser/browser/browsertab.py | 9 ++++----- qutebrowser/browser/commands.py | 2 +- qutebrowser/browser/downloads.py | 2 +- qutebrowser/browser/qtnetworkdownloads.py | 2 +- qutebrowser/browser/qutescheme.py | 2 +- qutebrowser/browser/shared.py | 10 +++++----- qutebrowser/browser/webkit/webkitelem.py | 2 +- qutebrowser/browser/webkit/webkitsettings.py | 2 +- qutebrowser/mainwindow/mainwindow.py | 15 +++++++-------- qutebrowser/mainwindow/messageview.py | 2 +- qutebrowser/mainwindow/prompt.py | 15 +++++++-------- qutebrowser/mainwindow/statusbar/bar.py | 6 +++--- qutebrowser/misc/crashdialog.py | 4 ++-- qutebrowser/misc/keyhintwidget.py | 6 +++--- 15 files changed, 39 insertions(+), 42 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index bdffc2921..70a4c046f 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -464,7 +464,7 @@ def _init_modules(args, crash_handler): completionmodels.init() log.init.debug("Misc initialization...") - if config.val.ui.hide_wayland_decoration: + if config.val.window.hide_wayland_decoration: os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' else: os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index fd8f8872f..c3ac426db 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -264,17 +264,17 @@ class AbstractZoom(QObject): def _on_config_changed(self, section, option): if section == 'ui' and option in ['zoom-levels', 'default-zoom']: if not self._default_zoom_changed: - factor = float(config.val.ui.default_zoom) / 100 + factor = float(config.val.zoom.default) / 100 self._set_factor_internal(factor) self._default_zoom_changed = False self._init_neighborlist() def _init_neighborlist(self): """Initialize self._neighborlist.""" - levels = config.val.ui.zoom_levels + levels = config.val.zoom.levels self._neighborlist = usertypes.NeighborList( levels, mode=usertypes.NeighborList.Modes.edge) - self._neighborlist.fuzzyval = config.val.ui.default_zoom + self._neighborlist.fuzzyval = config.val.zoom.default def offset(self, offset): """Increase/Decrease the zoom level by the given offset. @@ -310,8 +310,7 @@ class AbstractZoom(QObject): raise NotImplementedError def set_default(self): - default_zoom = config.val.ui.default_zoom - self._set_factor_internal(float(default_zoom) / 100) + self._set_factor_internal(float(config.val.zoom.default) / 100) class AbstractCaret(QObject): diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 185b7f5cd..93435f385 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -888,7 +888,7 @@ class CommandDispatcher: level = count if count is not None else zoom if level is None: - level = config.val.ui.default_zoom + level = config.val.zoom.default tab = self._current_widget() try: diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index b70d5ca45..9a9c63f13 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -735,7 +735,7 @@ class AbstractDownloadManager(QObject): download.remove_requested.connect(functools.partial( self._remove_item, download)) - delay = config.val.ui.remove_finished_downloads + delay = config.val.downloads.remove_finished if delay > -1: download.finished.connect( lambda: QTimer.singleShot(delay, download.remove)) diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 9aba6d335..bf41540e1 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -368,7 +368,7 @@ class DownloadManager(downloads.AbstractDownloadManager): super().__init__(parent) self._networkmanager = networkmanager.NetworkManager( win_id=win_id, tab_id=None, - private=config.val.private_browsing, parent=self) + private=config.val.content.private_browsing, parent=self) @pyqtSlot('QUrl') def get(self, url, *, user_agent=None, **kwargs): diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 552ced168..0b60848cc 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -285,7 +285,7 @@ def qute_history(url): return 'text/html', jinja.render( 'history.html', title='History', - session_interval=config.val.ui.history_session_interval + session_interval=config.val.history_session_interval ) else: # Get current date from query parameter, if not given choose today. diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 5d3ddc506..ab5080f55 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -72,7 +72,7 @@ def authentication_required(url, authenticator, abort_on): def javascript_confirm(url, js_msg, abort_on): """Display a javascript confirm prompt.""" log.js.debug("confirm: {}".format(js_msg)) - if config.val.ui.modal_js_dialog: + if config.val.content.javascript.modal_dialog: raise CallSuper msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), @@ -86,7 +86,7 @@ def javascript_confirm(url, js_msg, abort_on): def javascript_prompt(url, js_msg, default, abort_on): """Display a javascript prompt.""" log.js.debug("prompt: {}".format(js_msg)) - if config.val.ui.modal_js_dialog: + if config.val.content.javascript.modal_dialog: raise CallSuper if config.val.content.ignore_javascript_prompt: return (False, "") @@ -107,7 +107,7 @@ def javascript_prompt(url, js_msg, default, abort_on): def javascript_alert(url, js_msg, abort_on): """Display a javascript alert.""" log.js.debug("alert: {}".format(js_msg)) - if config.val.ui.modal_js_dialog: + if config.val.content.javascript.modal_dialog: raise CallSuper if config.val.content.ignore_javascript_alert: @@ -233,7 +233,7 @@ def get_tab(win_id, target): def get_user_stylesheet(): """Get the combined user-stylesheet.""" - filename = config.val.ui.user_stylesheet + filename = config.val.content.user_stylesheet if filename is None: css = '' @@ -241,7 +241,7 @@ def get_user_stylesheet(): with open(filename, 'r', encoding='utf-8') as f: css = f.read() - if config.val.ui.hide_scrollbar: + if config.val.scrolling.bar: css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }' return css diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 522847511..216da2dd9 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -168,7 +168,7 @@ class WebKitElement(webelem.AbstractWebElement): if width > 1 and height > 1: # fix coordinates according to zoom level zoom = self._elem.webFrame().zoomFactor() - if not config.val.ui.zoom_text_only: + if not config.val.zoom.text_only: rect["left"] *= zoom rect["top"] *= zoom width *= zoom diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 9da142932..9ce88b94b 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -132,7 +132,7 @@ def init(_args): QWebSettings.setOfflineStoragePath( os.path.join(data_path, 'offline-storage')) - if (config.val.private_browsing and + if (config.val.content.private_browsing and not qtutils.version_check('5.4.2')): # WORKAROUND for https://codereview.qt-project.org/#/c/108936/ # Won't work when private browsing is not enabled globally, but that's diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 67aa9e125..672696a2a 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -163,7 +163,7 @@ class MainWindow(QWidget): self._init_downloadmanager() self._downloadview = downloadview.DownloadView(self.win_id) - if config.val.private_browsing: + if config.val.content.private_browsing: # This setting always trumps what's passed in. private = True else: @@ -250,7 +250,7 @@ class MainWindow(QWidget): left = (self.width() - width) / 2 if centered else 0 height_padding = 20 - status_position = config.val.ui.status_position + status_position = config.val.statusbar.position if status_position == 'bottom': if self.status.isVisible(): status_height = self.status.height() @@ -341,10 +341,9 @@ class MainWindow(QWidget): self._vbox.removeWidget(self.tabbed_browser) self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self.status) - downloads_position = config.val.ui.downloads_position - status_position = config.val.ui.status_position widgets = [self.tabbed_browser] + downloads_position = config.val.downloads.position if downloads_position == 'top': widgets.insert(0, self._downloadview) elif downloads_position == 'bottom': @@ -352,6 +351,7 @@ class MainWindow(QWidget): else: raise ValueError("Invalid position {}!".format(downloads_position)) + status_position = config.val.statusbar.position if status_position == 'top': widgets.insert(0, self.status) elif status_position == 'bottom': @@ -536,23 +536,22 @@ class MainWindow(QWidget): if crashsignal.is_crashing: e.accept() return - confirm_quit = config.val.ui.confirm_quit tab_count = self.tabbed_browser.count() download_model = objreg.get('download-model', scope='window', window=self.win_id) download_count = download_model.running_downloads() quit_texts = [] # Ask if multiple-tabs are open - if 'multiple-tabs' in confirm_quit and tab_count > 1: + if 'multiple-tabs' in config.val.confirm_quit and tab_count > 1: quit_texts.append("{} {} open.".format( tab_count, "tab is" if tab_count == 1 else "tabs are")) # Ask if multiple downloads running - if 'downloads' in confirm_quit and download_count > 0: + if 'downloads' in config.val.confirm_quit and download_count > 0: quit_texts.append("{} {} running.".format( download_count, "download is" if download_count == 1 else "downloads are")) # Process all quit messages that user must confirm - if quit_texts or 'always' in confirm_quit: + if quit_texts or 'always' in config.val.confirm_quit: msg = jinja2.Template("""
    {% for text in quit_texts %} diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index ac29ce86b..a93295e41 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -126,7 +126,7 @@ class MessageView(QWidget): widget = Message(level, text, replace=replace, parent=self) self._vbox.addWidget(widget) widget.show() - if config.val.ui.message_timeout != 0: + if config.val.messages.timeout != 0: self._clear_timer.start() self._messages.append(widget) self._last_text = text diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 8a1ee66d4..3c6a4aba2 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -233,14 +233,13 @@ class PromptContainer(QWidget): """ STYLESHEET = """ - {% set prompt_radius = config.val.ui.prompt_radius %} QWidget#PromptContainer { - {% if config.val.ui.status_position == 'top' %} - border-bottom-left-radius: {{ prompt_radius }}px; - border-bottom-right-radius: {{ prompt_radius }}px; + {% if config.val.statusbar.position == 'top' %} + border-bottom-left-radius: {{ config.val.prompt.radius }}px; + border-bottom-right-radius: {{ config.val.prompt.radius }}px; {% else %} - border-top-left-radius: {{ prompt_radius }}px; - border-top-right-radius: {{ prompt_radius }}px; + border-top-left-radius: {{ config.val.prompt.radius }}px; + border-top-right-radius: {{ config.val.prompt.radius }}px; {% endif %} } @@ -566,7 +565,7 @@ class FilenamePrompt(_BasePrompt): self.setFocusProxy(self._lineedit) self._init_key_label() - if config.val.ui.prompt_filebrowser: + if config.val.ui.prompt.filebrowser: self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @pyqtSlot(str) @@ -628,7 +627,7 @@ class FilenamePrompt(_BasePrompt): self._file_view.setModel(self._file_model) self._file_view.clicked.connect(self._insert_path) - if config.val.ui.prompt_filebrowser: + if config.val.ui.prompt.filebrowser: self._vbox.addWidget(self._file_view) else: self._file_view.hide() diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 513b7ad83..29398bf07 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -211,15 +211,15 @@ class StatusBar(QWidget): @pyqtSlot() def maybe_hide(self): """Hide the statusbar if it's configured to do so.""" - hide = config.val.ui.hide_statusbar tab = self._current_tab() + hide = config.val.statusbar.hide if hide or (tab is not None and tab.data.fullscreen): self.hide() else: self.show() def _set_hbox_padding(self): - padding = config.val.ui.statusbar_padding + padding = config.val.statusbar.padding self._hbox.setContentsMargins(padding.left, 0, padding.right, 0) @pyqtProperty('QStringList') @@ -344,7 +344,7 @@ class StatusBar(QWidget): def minimumSizeHint(self): """Set the minimum height to the text height plus some padding.""" - padding = config.val.ui.statusbar_padding + padding = config.val.statusbar.padding width = super().minimumSizeHint().width() height = self.fontMetrics().height() + padding.top + padding.bottom return QSize(width, height) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 115796259..98fa83753 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -432,7 +432,7 @@ class ExceptionCrashDialog(_CrashDialog): self._chk_log = QCheckBox("Include a debug log in the report", checked=True) try: - if config.val.private_browsing: + if config.val.content.private_browsing: self._chk_log.setChecked(False) except Exception: log.misc.exception("Error while checking private browsing mode") @@ -524,7 +524,7 @@ class FatalCrashDialog(_CrashDialog): "accessed pages in the report.", checked=True) try: - if config.val.private_browsing: + if config.val.content.private_browsing: self._chk_history.setChecked(False) except Exception: log.misc.exception("Error while checking private browsing mode") diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 2dfede7b4..0c8ce2d49 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -51,7 +51,7 @@ class KeyHintView(QLabel): color: {{ color['keyhint.fg'] }}; background-color: {{ color['keyhint.bg'] }}; padding: 6px; - {% if config.val.ui.status_position == 'top' %} + {% if config.val.statusbar.position == 'top' %} border-bottom-right-radius: 6px; {% else %} border-top-right-radius: 6px; @@ -90,7 +90,7 @@ class KeyHintView(QLabel): self.hide() return - blacklist = config.val.ui.keyhint_blacklist or [] + blacklist = config.val.keyhint.blacklist or [] keyconf = objreg.get('key-config') def blacklisted(keychain): @@ -107,7 +107,7 @@ class KeyHintView(QLabel): return # delay so a quickly typed keychain doesn't display hints - self._show_timer.setInterval(config.val.ui.keyhint_delay) + self._show_timer.setInterval(config.val.keyhint.delay) self._show_timer.start() suffix_color = html.escape(config.val.colors.keyhint.fg.suffix) From aa6f229e6bd8d34a0b40c2c08988798a5e5a8efd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 13:05:24 +0200 Subject: [PATCH 020/516] Add utils.yaml_{load,dump} --- qutebrowser/config/configdata.py | 4 +--- qutebrowser/misc/sessions.py | 9 ++------- qutebrowser/utils/utils.py | 16 ++++++++++++++++ tests/unit/config/test_configdata.py | 4 ++++ tests/unit/utils/test_utils.py | 8 ++++++++ 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index bda605642..eea7ec01d 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -33,8 +33,6 @@ import sys import re import collections -import yaml - from qutebrowser.config import configtypes, sections from qutebrowser.utils import usertypes, qtutils, utils @@ -648,7 +646,7 @@ def _read_yaml(yaml_data): A dict mapping option names to Option elements. """ parsed = {} - data = yaml.load(yaml_data) + data = utils.yaml_load(yaml_data) keys = {'type', 'default', 'desc', 'backend'} diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 3bbee29f9..c9abfc830 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -26,10 +26,6 @@ import sip from PyQt5.QtCore import pyqtSignal, QUrl, QObject, QPoint, QTimer from PyQt5.QtWidgets import QApplication import yaml -try: - from yaml import CSafeLoader as YamlLoader, CSafeDumper as YamlDumper -except ImportError: # pragma: no cover - from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper from qutebrowser.utils import (standarddir, objreg, qtutils, log, usertypes, message, utils) @@ -298,8 +294,7 @@ class SessionManager(QObject): log.sessions.vdebug("Saving data: {}".format(data)) try: with qtutils.savefile_open(path) as f: - yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False, - encoding='utf-8', allow_unicode=True) + utils.yaml_dump(data, f) except (OSError, UnicodeEncodeError, yaml.YAMLError) as e: raise SessionError(e) else: @@ -387,7 +382,7 @@ class SessionManager(QObject): path = self._get_session_path(name, check_exists=True) try: with open(path, encoding='utf-8') as f: - data = yaml.load(f, Loader=YamlLoader) + data = utils.yaml_load(f, Loader=YamlLoader) except (OSError, UnicodeDecodeError, yaml.YAMLError) as e: raise SessionError(e) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 114be6a52..fbafa5353 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -36,6 +36,11 @@ from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices from PyQt5.QtWidgets import QApplication import pkg_resources +import yaml +try: + from yaml import CSafeLoader as YamlLoader, CSafeDumper as YamlDumper +except ImportError: # pragma: no cover + from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper import qutebrowser from qutebrowser.utils import qtutils, log @@ -876,3 +881,14 @@ def expand_windows_drive(path): return path + "\\" else: return path + + +def yaml_load(f): + """Wrapper over yaml.load using the C loader if possible.""" + return yaml.load(f, Loader=YamlLoader) + + +def yaml_dump(data, f=None): + """Wrapper over yaml.dump using the C dumper if possible.""" + return yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False, + encoding='utf-8', allow_unicode=True) diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py index 81b680f08..87231f82f 100644 --- a/tests/unit/config/test_configdata.py +++ b/tests/unit/config/test_configdata.py @@ -34,6 +34,10 @@ def test_init(): assert 'ignore_case' in configdata.DATA +def test_init_benchmark(benchmark): + benchmark(configdata.init) + + class TestReadYaml: diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 4b03dcf8c..aff5b49b4 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -936,3 +936,11 @@ class TestOpenFile: ]) def test_expand_windows_drive(path, expected): assert utils.expand_windows_drive(path) == expected + + +def test_yaml_load(): + assert utils.yaml_load("[1, 2]") == [1, 2] + + +def test_yaml_dump(): + assert utils.yaml_dump([1, 2]) == b'- 1\n- 2\n' From f1d81d86aae050b5c4f060a843bba894c209e072 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 13:07:31 +0200 Subject: [PATCH 021/516] Fix configtypes _basic_validation --- qutebrowser/config/configtypes.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 1969ec946..75d8a1720 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -116,22 +116,18 @@ class BaseType: value: The value to check. pytype: If given, a Python type to check the value against. """ - if not value: - if self.none_ok: - return - else: - raise configexc.ValidationError(value, "may not be empty!") - - if isinstance(value, str): - if any(ord(c) < 32 or ord(c) == 0x7f for c in value): - raise configexc.ValidationError(value, "may not contain " - "unprintable chars!") - if pytype is not None and not isinstance(value, pytype): raise configexc.ValidationError( value, "expected a value of type {} but got {}".format( pytype, type(value))) + if isinstance(value, str): + if not value and not self.none_ok: + raise configexc.ValidationError(value, "may not be empty!") + if any(ord(c) < 32 or ord(c) == 0x7f for c in value): + raise configexc.ValidationError(value, "may not contain " + "unprintable chars!") + def _validate_valid_values(self, value): """Validate value against possible values. From 129ee33ffb823f2ce5c34bdb6f75403d9eda6c5f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 13:12:37 +0200 Subject: [PATCH 022/516] Refactor former network section --- qutebrowser/browser/network/proxy.py | 6 +++--- qutebrowser/browser/shared.py | 8 ++++---- qutebrowser/browser/webengine/interceptor.py | 2 +- qutebrowser/browser/webengine/webenginesettings.py | 3 +-- qutebrowser/browser/webkit/network/networkmanager.py | 4 ++-- qutebrowser/browser/webkit/webpage.py | 2 +- qutebrowser/commands/userscripts.py | 2 +- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index 037c9c712..2821a840d 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -44,7 +44,7 @@ class ProxyFactory(QNetworkProxyFactory): Return: None if proxy is correct, otherwise an error message. """ - proxy = config.val.network.proxy + proxy = config.val.content.proxy if isinstance(proxy, pac.PACFetcher): return proxy.fetch_error() else: @@ -59,7 +59,7 @@ class ProxyFactory(QNetworkProxyFactory): Return: A list of QNetworkProxy objects in order of preference. """ - proxy = config.val.network.proxy + proxy = config.val.content.proxy if proxy is configtypes.SYSTEM_PROXY: proxies = QNetworkProxyFactory.systemProxyForQuery(query) elif isinstance(proxy, pac.PACFetcher): @@ -69,7 +69,7 @@ class ProxyFactory(QNetworkProxyFactory): for p in proxies: if p.type() != QNetworkProxy.NoProxy: capabilities = p.capabilities() - if config.val.network.proxy_dns_requests: + if config.val.content.proxy_dns_requests: capabilities |= QNetworkProxy.HostNameLookupCapability else: capabilities &= ~QNetworkProxy.HostNameLookupCapability diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index ab5080f55..4ddc22786 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -35,16 +35,16 @@ class CallSuper(Exception): def custom_headers(): """Get the combined custom headers.""" headers = {} - dnt = b'1' if config.val.network.do_not_track else b'0' + dnt = b'1' if config.val.content.do_not_track else b'0' headers[b'DNT'] = dnt headers[b'X-Do-Not-Track'] = dnt - config_headers = config.val.network.custom_headers + config_headers = config.val.content.custom_headers if config_headers is not None: for header, value in config_headers.items(): headers[header.encode('ascii')] = value.encode('ascii') - accept_language = config.val.network.accept_language + accept_language = config.val.content.accept_language if accept_language is not None: headers[b'Accept-Language'] = accept_language.encode('ascii') @@ -129,7 +129,7 @@ def ignore_certificate_errors(url, errors, abort_on): Return: True if the error should be ignored, False otherwise. """ - ssl_strict = config.val.network.ssl_strict + ssl_strict = config.val.content.ssl_strict log.webview.debug("Certificate errors {!r}, strict {}".format( errors, ssl_strict)) diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 5d9dc8a70..71744f5d7 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -63,6 +63,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): for header, value in shared.custom_headers(): info.setHttpHeader(header, value) - user_agent = config.val.network.user_agent + user_agent = config.val.conent.user_agent if user_agent is not None: info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 00f4ec423..7fc68f6fc 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -163,8 +163,7 @@ def _set_user_agent(profile): per-domain user agents), but this one still gets used for things like window.navigator.userAgent in JS. """ - user_agent = config.val.network.user_agent - profile.setHttpUserAgent(user_agent) + profile.setHttpUserAgent(config.val.content.user_agent) def update_settings(section, option): diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index c2ad03a6b..858dc2176 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -274,7 +274,7 @@ class NetworkManager(QNetworkAccessManager): # altogether. reply.netrc_used = True try: - net = netrc.netrc(config.val.network.netrc_file) + net = netrc.netrc(config.val.content.netrc_file) authenticators = net.authenticators(reply.url().host()) if authenticators is not None: (user, _account, password) = authenticators @@ -338,7 +338,7 @@ class NetworkManager(QNetworkAccessManager): def set_referer(self, req, current_url): """Set the referer header.""" - referer_header_conf = config.val.network.referer_header + referer_header_conf = config.val.content.referer_header try: if referer_header_conf == 'never': diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index beb47ea09..ac156d12f 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -384,7 +384,7 @@ class BrowserPage(QWebPage): def userAgentForUrl(self, url): """Override QWebPage::userAgentForUrl to customize the user agent.""" - ua = config.val.network.user_agent + ua = config.val.content.user_agent if ua is None: return super().userAgentForUrl(url) else: diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 03d6bbffc..3e80946df 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -417,7 +417,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False): lambda cmd: log.commands.debug("Got userscript command: {}".format(cmd))) runner.got_cmd.connect(commandrunner.run_safely) - user_agent = config.val.network.user_agent + user_agent = config.val.content.user_agent if user_agent is not None: env['QUTE_USER_AGENT'] = user_agent From af134eb861ee7fb8e7cb43ab64cb97a639772632 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 13:47:06 +0200 Subject: [PATCH 023/516] Refactor websettings for new config --- .../browser/webengine/webenginesettings.py | 143 +++++++------- qutebrowser/browser/webkit/webkitsettings.py | 174 ++++++++---------- qutebrowser/config/config.py | 6 +- qutebrowser/config/newconfig.py | 4 +- qutebrowser/config/websettings.py | 16 +- 5 files changed, 158 insertions(+), 185 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 7fc68f6fc..c940eb508 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -166,13 +166,13 @@ def _set_user_agent(profile): profile.setHttpUserAgent(config.val.content.user_agent) -def update_settings(section, option): +def update_settings(option): """Update global settings when qwebsettings changed.""" - websettings.update_mappings(MAPPINGS, section, option) - if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: + websettings.update_mappings(MAPPINGS, option) + if option in ['scrollbar.hide', 'content.user_stylesheet']: _init_stylesheet(default_profile) _init_stylesheet(private_profile) - elif section == 'network' and option == 'user-agent': + elif option == 'content.user_agent': _set_user_agent(default_profile) _set_user_agent(private_profile) @@ -211,7 +211,7 @@ def init(args): # We need to do this here as a WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-58650 if not qtutils.version_check('5.9'): - PersistentCookiePolicy().set(config.val.content.cookies_store) + PersistentCookiePolicy().set(config.val.content.cookies.store) Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) websettings.init_mappings(MAPPINGS) @@ -236,79 +236,70 @@ def shutdown(): MAPPINGS = { - 'content': { - 'allow-images': - Attribute(QWebEngineSettings.AutoLoadImages), - 'allow-javascript': - Attribute(QWebEngineSettings.JavascriptEnabled), - 'javascript-can-open-windows-automatically': - Attribute(QWebEngineSettings.JavascriptCanOpenWindows), - 'javascript-can-access-clipboard': - Attribute(QWebEngineSettings.JavascriptCanAccessClipboard), - 'allow-plugins': - Attribute(QWebEngineSettings.PluginsEnabled), - 'hyperlink-auditing': - Attribute(QWebEngineSettings.HyperlinkAuditingEnabled), - 'local-content-can-access-remote-urls': - Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls), - 'local-content-can-access-file-urls': - Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls), - 'webgl': - Attribute(QWebEngineSettings.WebGLEnabled), - }, - 'input': { - 'spatial-navigation': - Attribute(QWebEngineSettings.SpatialNavigationEnabled), - 'links-included-in-focus-chain': - Attribute(QWebEngineSettings.LinksIncludedInFocusChain), - }, - 'fonts': { - 'web-family-standard': - FontFamilySetter(QWebEngineSettings.StandardFont), - 'web-family-fixed': - FontFamilySetter(QWebEngineSettings.FixedFont), - 'web-family-serif': - FontFamilySetter(QWebEngineSettings.SerifFont), - 'web-family-sans-serif': - FontFamilySetter(QWebEngineSettings.SansSerifFont), - 'web-family-cursive': - FontFamilySetter(QWebEngineSettings.CursiveFont), - 'web-family-fantasy': - FontFamilySetter(QWebEngineSettings.FantasyFont), - 'web-size-minimum': - Setter(QWebEngineSettings.setFontSize, - args=[QWebEngineSettings.MinimumFontSize]), - 'web-size-minimum-logical': - Setter(QWebEngineSettings.setFontSize, - args=[QWebEngineSettings.MinimumLogicalFontSize]), - 'web-size-default': - Setter(QWebEngineSettings.setFontSize, - args=[QWebEngineSettings.DefaultFontSize]), - 'web-size-default-fixed': - Setter(QWebEngineSettings.setFontSize, - args=[QWebEngineSettings.DefaultFixedFontSize]), - }, - 'ui': { - 'smooth-scrolling': - Attribute(QWebEngineSettings.ScrollAnimatorEnabled), - }, - 'storage': { - 'local-storage': - Attribute(QWebEngineSettings.LocalStorageEnabled), - 'cache-size': - # 0: automatically managed by QtWebEngine - DefaultProfileSetter('setHttpCacheMaximumSize', default=0), - }, - 'general': { - 'xss-auditing': - Attribute(QWebEngineSettings.XSSAuditingEnabled), - 'default-encoding': - Setter(QWebEngineSettings.setDefaultTextEncoding), - } + 'content.images': + Attribute(QWebEngineSettings.AutoLoadImages), + 'content.javascript.enabled': + Attribute(QWebEngineSettings.JavascriptEnabled), + 'content.javascript.can_open_windows_automatically': + Attribute(QWebEngineSettings.JavascriptCanOpenWindows), + 'content.javascript.can_access_clipboard': + Attribute(QWebEngineSettings.JavascriptCanAccessClipboard), + 'content.plugins': + Attribute(QWebEngineSettings.PluginsEnabled), + 'content.hyperlink_auditing': + Attribute(QWebEngineSettings.HyperlinkAuditingEnabled), + 'content.local_content_can_access_remote_urls': + Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls), + 'content.local_content_can_access_file_urls': + Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls), + 'content.webgl': + Attribute(QWebEngineSettings.WebGLEnabled), + 'content.local_storage': + Attribute(QWebEngineSettings.LocalStorageEnabled), + 'content.cache_size': + # 0: automatically managed by QtWebEngine + DefaultProfileSetter('setHttpCacheMaximumSize', default=0), + 'content.xss_auditing': + Attribute(QWebEngineSettings.XSSAuditingEnabled), + 'content.default_encoding': + Setter(QWebEngineSettings.setDefaultTextEncoding), + + 'input.spatial_navigation': + Attribute(QWebEngineSettings.SpatialNavigationEnabled), + 'input.links_included_in_focus_chain': + Attribute(QWebEngineSettings.LinksIncludedInFocusChain), + + 'fonts.web.family.standard': + FontFamilySetter(QWebEngineSettings.StandardFont), + 'fonts.web.family.fixed': + FontFamilySetter(QWebEngineSettings.FixedFont), + 'fonts.web.family.serif': + FontFamilySetter(QWebEngineSettings.SerifFont), + 'fonts.web.family.sans_serif': + FontFamilySetter(QWebEngineSettings.SansSerifFont), + 'fonts.web.family.cursive': + FontFamilySetter(QWebEngineSettings.CursiveFont), + 'fonts.web.family.fantasy': + FontFamilySetter(QWebEngineSettings.FantasyFont), + 'fonts.web.size.minimum': + Setter(QWebEngineSettings.setFontSize, + args=[QWebEngineSettings.MinimumFontSize]), + 'fonts.web.size.minimum_logical': + Setter(QWebEngineSettings.setFontSize, + args=[QWebEngineSettings.MinimumLogicalFontSize]), + 'fonts.web.size.default': + Setter(QWebEngineSettings.setFontSize, + args=[QWebEngineSettings.DefaultFontSize]), + 'fonts.web.size.default_fixed': + Setter(QWebEngineSettings.setFontSize, + args=[QWebEngineSettings.DefaultFixedFontSize]), + + 'scrolling.smooth': + Attribute(QWebEngineSettings.ScrollAnimatorEnabled), } try: - MAPPINGS['general']['print-element-backgrounds'] = Attribute( + MAPPINGS['content.print_element_backgrounds'] = Attribute( QWebEngineSettings.PrintElementBackgrounds) except AttributeError: # Added in Qt 5.8 @@ -317,4 +308,4 @@ except AttributeError: if qtutils.version_check('5.9'): # https://bugreports.qt.io/browse/QTBUG-58650 - MAPPINGS['content']['cookies-store'] = PersistentCookiePolicy() + MAPPINGS['content.cookies.store'] = PersistentCookiePolicy() diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 9ce88b94b..a51c7384f 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -111,12 +111,11 @@ def _set_user_stylesheet(): QWebSettings.globalSettings().setUserStyleSheetUrl(url) -def update_settings(section, option): +def update_settings(option): """Update global settings when qwebsettings changed.""" - if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: + if option in ['scrollbar.hide', 'content.user_stylesheet']: _set_user_stylesheet() - - websettings.update_mappings(MAPPINGS, section, option) + websettings.update_mappings(MAPPINGS, option) def init(_args): @@ -152,96 +151,79 @@ def shutdown(): MAPPINGS = { - 'content': { - 'allow-images': - Attribute(QWebSettings.AutoLoadImages), - 'allow-javascript': - Attribute(QWebSettings.JavascriptEnabled), - 'javascript-can-open-windows-automatically': - Attribute(QWebSettings.JavascriptCanOpenWindows), - 'javascript-can-close-windows': - Attribute(QWebSettings.JavascriptCanCloseWindows), - 'javascript-can-access-clipboard': - Attribute(QWebSettings.JavascriptCanAccessClipboard), - 'allow-plugins': - Attribute(QWebSettings.PluginsEnabled), - 'webgl': - Attribute(QWebSettings.WebGLEnabled), - 'hyperlink-auditing': - Attribute(QWebSettings.HyperlinkAuditingEnabled), - 'local-content-can-access-remote-urls': - Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), - 'local-content-can-access-file-urls': - Attribute(QWebSettings.LocalContentCanAccessFileUrls), - 'cookies-accept': - CookiePolicy(), - }, - 'network': { - 'dns-prefetch': - Attribute(QWebSettings.DnsPrefetchEnabled), - }, - 'input': { - 'spatial-navigation': - Attribute(QWebSettings.SpatialNavigationEnabled), - 'links-included-in-focus-chain': - Attribute(QWebSettings.LinksIncludedInFocusChain), - }, - 'fonts': { - 'web-family-standard': - FontFamilySetter(QWebSettings.StandardFont), - 'web-family-fixed': - FontFamilySetter(QWebSettings.FixedFont), - 'web-family-serif': - FontFamilySetter(QWebSettings.SerifFont), - 'web-family-sans-serif': - FontFamilySetter(QWebSettings.SansSerifFont), - 'web-family-cursive': - FontFamilySetter(QWebSettings.CursiveFont), - 'web-family-fantasy': - FontFamilySetter(QWebSettings.FantasyFont), - 'web-size-minimum': - Setter(QWebSettings.setFontSize, - args=[QWebSettings.MinimumFontSize]), - 'web-size-minimum-logical': - Setter(QWebSettings.setFontSize, - args=[QWebSettings.MinimumLogicalFontSize]), - 'web-size-default': - Setter(QWebSettings.setFontSize, - args=[QWebSettings.DefaultFontSize]), - 'web-size-default-fixed': - Setter(QWebSettings.setFontSize, - args=[QWebSettings.DefaultFixedFontSize]), - }, - 'ui': { - 'zoom-text-only': - Attribute(QWebSettings.ZoomTextOnly), - 'frame-flattening': - Attribute(QWebSettings.FrameFlatteningEnabled), - # user-stylesheet is handled separately - 'smooth-scrolling': - Attribute(QWebSettings.ScrollAnimatorEnabled), - #'accelerated-compositing': - # Attribute(QWebSettings.AcceleratedCompositingEnabled), - #'tiled-backing-store': - # Attribute(QWebSettings.TiledBackingStoreEnabled), - }, - 'storage': { - 'offline-web-application-cache': - Attribute(QWebSettings.OfflineWebApplicationCacheEnabled), - 'local-storage': - Attribute(QWebSettings.LocalStorageEnabled, - QWebSettings.OfflineStorageDatabaseEnabled), - 'maximum-pages-in-cache': - StaticSetter(QWebSettings.setMaximumPagesInCache), - }, - 'general': { - 'developer-extras': - Attribute(QWebSettings.DeveloperExtrasEnabled), - 'print-element-backgrounds': - Attribute(QWebSettings.PrintElementBackgrounds), - 'xss-auditing': - Attribute(QWebSettings.XSSAuditingEnabled), - 'default-encoding': - Setter(QWebSettings.setDefaultTextEncoding), - } + 'content.images': + Attribute(QWebSettings.AutoLoadImages), + 'content.javascript.enabled': + Attribute(QWebSettings.JavascriptEnabled), + 'content.javascript.can_open_windows_automatically': + Attribute(QWebSettings.JavascriptCanOpenWindows), + 'content.javascript.can_close_windows': + Attribute(QWebSettings.JavascriptCanCloseWindows), + 'content.javascript.can_access_clipboard': + Attribute(QWebSettings.JavascriptCanAccessClipboard), + 'content.plugins': + Attribute(QWebSettings.PluginsEnabled), + 'content.webgl': + Attribute(QWebSettings.WebGLEnabled), + 'content.hyperlink_auditing': + Attribute(QWebSettings.HyperlinkAuditingEnabled), + 'content.local_content_can_access_remote_urls': + Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), + 'content.local_content_can_access_file_urls': + Attribute(QWebSettings.LocalContentCanAccessFileUrls), + 'content.cookies.accept': + CookiePolicy(), + 'content.dns_prefetch': + Attribute(QWebSettings.DnsPrefetchEnabled), + 'content.frame-flattening': + Attribute(QWebSettings.FrameFlatteningEnabled), + 'content.offline_web_application_cache': + Attribute(QWebSettings.OfflineWebApplicationCacheEnabled), + 'content.local_storage': + Attribute(QWebSettings.LocalStorageEnabled, + QWebSettings.OfflineStorageDatabaseEnabled), + 'content.maximum_pages_in_cache': + StaticSetter(QWebSettings.setMaximumPagesInCache), + 'content.developer_extras': + Attribute(QWebSettings.DeveloperExtrasEnabled), + 'content.print_element_backgrounds': + Attribute(QWebSettings.PrintElementBackgrounds), + 'content.xss_auditing': + Attribute(QWebSettings.XSSAuditingEnabled), + 'content.default_encoding': + Setter(QWebSettings.setDefaultTextEncoding), + # content.user_stylesheet is handled separately + + 'input.spatial_navigation': + Attribute(QWebSettings.SpatialNavigationEnabled), + 'input.links_included_in_focus_chain': + Attribute(QWebSettings.LinksIncludedInFocusChain), + + 'fonts.web.family.standard': + FontFamilySetter(QWebSettings.StandardFont), + 'fonts.web.family.fixed': + FontFamilySetter(QWebSettings.FixedFont), + 'fonts.web.family.serif': + FontFamilySetter(QWebSettings.SerifFont), + 'fonts.web.family.sans_serif': + FontFamilySetter(QWebSettings.SansSerifFont), + 'fonts.web.family.cursive': + FontFamilySetter(QWebSettings.CursiveFont), + 'fonts.web.family.fantasy': + FontFamilySetter(QWebSettings.FantasyFont), + 'fonts.web.size.minimum': + Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumFontSize]), + 'fonts.web.size.minimum_logical': + Setter(QWebSettings.setFontSize, + args=[QWebSettings.MinimumLogicalFontSize]), + 'fonts.web.size.default': + Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFontSize]), + 'fonts.web.size.default_fixed': + Setter(QWebSettings.setFontSize, + args=[QWebSettings.DefaultFixedFontSize]), + + 'zoom.text_only': + Attribute(QWebSettings.ZoomTextOnly), + 'scrolling.smooth': + Attribute(QWebSettings.ScrollAnimatorEnabled), } diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index f7b9cd93e..e223a3b14 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -55,8 +55,7 @@ UNSET = object() # FIXME:conf for new config val = None - - +instance = None def get(*args, **kwargs): @@ -183,11 +182,12 @@ def init(parent=None): Args: parent: The parent to pass to QObjects which get initialized. """ - global val + global val, instance # _init_main_config(parent) configdata.init() newconfig.init(parent) val = newconfig.val + instance = newconfig.instance _init_key_config(parent) _init_misc() diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index c10cee770..c06eee55d 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -28,6 +28,7 @@ from qutebrowser.utils import utils, objreg # An easy way to access the config from other code via config.val.foo val = None +instance = None _change_filters = [] @@ -192,8 +193,9 @@ def init(parent): new_config.read_defaults() objreg.register('config', new_config) - global val + global val, instance val = ConfigContainer(new_config) + instance = new_config for cf in _change_filters: cf.validate() diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index d2ab52ccc..0e57af45f 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -192,21 +192,19 @@ class FontFamilySetter(Setter): def init_mappings(mappings): """Initialize all settings based on a settings mapping.""" - for sectname, section in mappings.items(): - for optname, mapping in section.items(): - value = config.get(sectname, optname) - log.config.vdebug("Setting {} -> {} to {!r}".format( - sectname, optname, value)) - mapping.set(value) + for option, mapping in mappings.items(): + value = config.instance.get(option) + log.config.vdebug("Setting {} to {!r}".format(option, value)) + mapping.set(value) -def update_mappings(mappings, section, option): +def update_mappings(mappings, option): """Update global settings when QWeb(Engine)Settings changed.""" try: - mapping = mappings[section][option] + mapping = mappings[option] except KeyError: return - value = config.get(section, option) + value = config.instance.get(option) mapping.set(value) From 231b7303f5345453206578c57376c74768213d04 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 13:51:05 +0200 Subject: [PATCH 024/516] Use null for empty config values. --- qutebrowser/config/configdata.yml | 28 ++++++++++++++-------------- qutebrowser/config/configtypes.py | 6 +++++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 02d819b9a..632e08013 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -34,10 +34,10 @@ downloads.open_dispatcher: type: name: String none_ok: true - default: "" + default: null desc: >- - The default program used to open downloads. Set to an empty string to use - the default internal handler. + The default program used to open downloads. If unset, the default internal + handler is used. Any `{}` in the string will be expanded to the filename, else the filename will be appended. @@ -184,8 +184,8 @@ session_default_name: type: name: SessionName none_ok: true - default: "" - desc: The name of the session to save by default, or empty for the last loaded + default: null + desc: The name of the session to save by default, or unset for the last loaded session. url_incdec_segments: @@ -285,7 +285,7 @@ content.user_stylesheet: type: name: File none_ok: True - default: "" + default: null desc: User stylesheet to use (absolute filename or filename relative to the config directory). Will expand environment variables. @@ -367,7 +367,7 @@ keyhint.blacklist: valtype: name: String none_ok: true - default: "" + default: null desc: >- Keychains that shouldn\'t be shown in the keyhint dialog. @@ -419,7 +419,7 @@ content.referer_header: desc: Send the Referer header content.user_agent: - default: "" + default: null type: name: String none_ok: true @@ -467,7 +467,7 @@ content.user_agent: - - "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko" - IE 11.0 for Desktop Win7 64-bit - desc: User agent to send. Empty to send the default. + desc: User agent to send. Unset to send the default. content.proxy: default: system @@ -516,11 +516,11 @@ content.custom_headers: desc: Set custom headers for qutebrowser HTTP requests. content.netrc_file: - default: "" + default: null type: name: File none_ok: true - desc: Set location of a netrc-file for HTTP authentication. If empty, ~/.netrc + desc: Set location of a netrc-file for HTTP authentication. If unset, ~/.netrc is used. @@ -889,12 +889,12 @@ tabs.indicator_padding: # storage downloads.location.directory: - default: "" + default: null type: name: Directory none_ok: true - desc: The directory to save downloads to. An empty value selects a sensible os-specific - default. Will expand environment variables. + desc: The directory to save downloads to. If unset, a sensible os-specific + default is used. Will expand environment variables. downloads.location.prompt: default: true diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 75d8a1720..1fc156a40 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -116,7 +116,11 @@ class BaseType: value: The value to check. pytype: If given, a Python type to check the value against. """ - if pytype is not None and not isinstance(value, pytype): + if value is None and not self.none_ok: + raise configexc.ValidationError(value, "may not be empty!") + + if (value is not None and pytype is not None and + not isinstance(value, pytype)): raise configexc.ValidationError( value, "expected a value of type {} but got {}".format( pytype, type(value))) From 26bf588fadf40d8e51f7f010d810b8a8856b7ef3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 13:52:06 +0200 Subject: [PATCH 025/516] Fix _validate_valid_values --- qutebrowser/config/configtypes.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 1fc156a40..ff7a64807 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -141,17 +141,11 @@ class BaseType: Args: value: The value to validate. """ - # FIXME:conf still needed? - if not value: - return if self.valid_values is not None: if value not in self.valid_values: raise configexc.ValidationError( value, "valid values: {}".format(', '.join(self.valid_values))) - else: - raise NotImplementedError("{} does not implement validate.".format( - self.__class__.__name__)) def from_str(self, value): """Get the setting value from a string. From a3d4822b9fdc3df7a7df28324d68cdd8e9c07508 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 13:54:56 +0200 Subject: [PATCH 026/516] Fix up adblock settings --- qutebrowser/browser/adblock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index cd4380fe3..465cc0b81 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -67,7 +67,7 @@ def is_whitelisted_host(host): Args: host: The host of the request as string. """ - whitelist = config.val.content.host_blocking_whitelist + whitelist = config.val.content.host_blocking.whitelist if whitelist is None: return False @@ -123,7 +123,7 @@ class HostBlocker: def is_blocked(self, url): """Check if the given URL (as QUrl) is blocked.""" - if not config.val.content.host_blocking_enabled: + if not config.val.content.host_blocking.enabled: return False host = url.host() return ((host in self._blocked_hosts or @@ -164,9 +164,9 @@ class HostBlocker: if not found: args = objreg.get('args') - if (config.val.content.host_block_lists is not None and + if (config.val.content.host_blocking.lists is not None and args.basedir is None and - config.val.content.host_blocking_enabled): + config.val.content.host_blocking.enabled): message.info("Run :adblock-update to get adblock lists.") @cmdutils.register(instance='host-blocker') From e2b0fdf8aa16531c7eef6068e251040d34c1d528 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 14:00:18 +0200 Subject: [PATCH 027/516] Fix VerticalPosition and NewTabPosition Make them string so they have to_py() defined. --- qutebrowser/config/configtypes.py | 13 +++++++------ qutebrowser/mainwindow/tabbedbrowser.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index ff7a64807..d114904a3 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -252,8 +252,9 @@ class String(BaseType): completions: completions to be used, or None """ - def __init__(self, minlen=None, maxlen=None, forbidden=None, encoding=None, - none_ok=False, completions=None, valid_values=None): + def __init__(self, *, minlen=None, maxlen=None, forbidden=None, + encoding=None, none_ok=False, completions=None, + valid_values=None): super().__init__(none_ok) self.valid_values = valid_values @@ -1181,12 +1182,12 @@ class TextAlignment(MappingType): valid_values=ValidValues('left', 'right', 'center')) -class VerticalPosition(BaseType): +class VerticalPosition(String): """The position of the download bar.""" def __init__(self, none_ok=False): - super().__init__(none_ok) + super().__init__(none_ok=none_ok) self.valid_values = ValidValues('top', 'bottom') @@ -1282,12 +1283,12 @@ class ConfirmQuit(FlagList): return values -class NewTabPosition(BaseType): +class NewTabPosition(String): """How new tabs are positioned.""" def __init__(self, none_ok=False): - super().__init__(none_ok) + super().__init__(none_ok=none_ok) self.valid_values = ValidValues( ('prev', "Before the current tab."), ('next', "After the current tab."), diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 4a1f5a921..bfc402e3b 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -460,9 +460,9 @@ class TabbedBrowser(tabwidget.TabWidget): The index of the new tab. """ if explicit: - pos = config.val.tabs.new_tab_position_explicit + pos = config.val.tabs.new_position_explicit else: - pos = config.val.tabs.new_tab_position + pos = config.val.tabs.new_position if pos == 'prev': idx = self._tab_insert_idx_left # On first sight, we'd think we have to decrement From 61fe40f4a10fe1eb61576feb7319a834b459e6f7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 14:38:58 +0200 Subject: [PATCH 028/516] Initial stylesheet refactoring for new config --- qutebrowser/browser/downloadview.py | 4 +-- qutebrowser/browser/hints.py | 10 +++--- qutebrowser/completion/completiondelegate.py | 2 +- qutebrowser/completion/completionwidget.py | 28 ++++++++--------- qutebrowser/config/configdata.yml | 2 +- qutebrowser/config/style.py | 33 +------------------- qutebrowser/html/settings.html | 1 + qutebrowser/mainwindow/messageview.py | 24 +++++++------- qutebrowser/mainwindow/prompt.py | 20 ++++++------ qutebrowser/mainwindow/statusbar/bar.py | 26 +++++++-------- qutebrowser/mainwindow/statusbar/progress.py | 4 +-- qutebrowser/mainwindow/statusbar/url.py | 12 +++---- qutebrowser/misc/keyhintwidget.py | 10 +++--- 13 files changed, 73 insertions(+), 103 deletions(-) diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index 199f4d1d8..2c39de163 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -64,8 +64,8 @@ class DownloadView(QListView): STYLESHEET = """ QListView { - background-color: {{ color['downloads.bg.bar'] }}; - font: {{ font['downloads'] }}; + background-color: {{ conf.colors.downloads.bar.bg }}; + font: {{ conf.fonts.downloads }}; } QListView::item { diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index aed554d9f..625f9097f 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -65,10 +65,10 @@ class HintLabel(QLabel): STYLESHEET = """ QLabel { - background-color: {{ color['hints.bg'] }}; - color: {{ color['hints.fg'] }}; - font: {{ font['hints'] }}; - border: {{ config.val.hints.border }}; + background-color: {{ conf.colors.hints.bg }}; + color: {{ conf.colors.hints.fg }}; + font: {{ conf.fonts.hints }}; + border: {{ conf.hints.border }}; padding-left: -3px; padding-right: -3px; } @@ -108,7 +108,7 @@ class HintLabel(QLabel): matched = html.escape(matched) unmatched = html.escape(unmatched) - match_color = html.escape(config.val.colors.hints.fg.match) + match_color = html.escape(config.val.colors.hints.match.fg) self.setText('{}{}'.format( match_color, matched, unmatched)) self.adjustSize() diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index d0de544a7..343c7d359 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -189,7 +189,7 @@ class CompletionItemDelegate(QStyledItemDelegate): self._doc.setDefaultTextOption(text_option) self._doc.setDefaultStyleSheet(style.get_stylesheet(""" .highlight { - color: {{ color['completion.match.fg'] }}; + color: {{ conf.colors.completion.match.fg }}; } """)) self._doc.setDocumentMargin(2) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 7fc5f36c9..eedd21e94 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -57,27 +57,27 @@ class CompletionView(QTreeView): # don't define that in this stylesheet. STYLESHEET = """ QTreeView { - font: {{ font['completion'] }}; - background-color: {{ color['completion.bg'] }}; - alternate-background-color: {{ color['completion.alternate-bg'] }}; + font: {{ conf.fonts.completion.entry }}; + background-color: {{ conf.colors.completion.bg }}; + alternate-background-color: {{ conf.colors.completion.alternate_bg }}; outline: 0; border: 0px; } QTreeView::item:disabled { - background-color: {{ color['completion.category.bg'] }}; + background-color: {{ conf.colors.completion.category.bg }}; border-top: 1px solid - {{ color['completion.category.border.top'] }}; + {{ conf.colors.completion.category.border.top }}; border-bottom: 1px solid - {{ color['completion.category.border.bottom'] }}; + {{ conf.colors.completion.category.border.bottom }}; } QTreeView::item:selected, QTreeView::item:selected:hover { border-top: 1px solid - {{ color['completion.item.selected.border.top'] }}; + {{ conf.colors.completion.item.selected.border.top }}; border-bottom: 1px solid - {{ color['completion.item.selected.border.bottom'] }}; - background-color: {{ color['completion.item.selected.bg'] }}; + {{ conf.colors.completion.item.selected.border.bottom }}; + background-color: {{ conf.colors.completion.item.selected.bg }}; } QTreeView:item::hover { @@ -85,14 +85,14 @@ class CompletionView(QTreeView): } QTreeView QScrollBar { - width: {{ config.val.completion.scrollbar_width }}px; - background: {{ color['completion.scrollbar.bg'] }}; + width: {{ conf.completion.scrollbar.width }}px; + background: {{ conf.colors.completion.scrollbar.bg }}; } QTreeView QScrollBar::handle { - background: {{ color['completion.scrollbar.fg'] }}; - border: {{ config.val.completion.scrollbar_padding }}px solid - {{ color['completion.scrollbar.bg'] }}; + background: {{ conf.colors.completion.scrollbar.fg }}; + border: {{ conf.completion.scrollbar.padding }}px solid + {{ conf.colors.completion.scrollbar.bg }}; min-height: 10px; } diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 632e08013..0c4c6924c 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1409,7 +1409,7 @@ colors.statusbar.url.success.http.fg: type: QssColor desc: Foreground color of the URL in the statusbar on successful load (http). -colors.statusbar.url.success.https.bg: +colors.statusbar.url.success.https.fg: default: lime type: QssColor desc: Foreground color of the URL in the statusbar on successful load (https). diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index cc66af2d8..e36aebcbf 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -40,10 +40,8 @@ def get_stylesheet(template_str): Return: The formatted template as string. """ - colordict = ColorDict(config) template = jinja2.Template(template_str) - return template.render(color=colordict, font=config.section('fonts'), - config=objreg.get('config')) + return template.render(conf=config.val) def set_register_stylesheet(obj): @@ -68,32 +66,3 @@ def _update_stylesheet(obj): get_stylesheet.cache_clear() if not sip.isdeleted(obj): obj.setStyleSheet(get_stylesheet(obj.STYLESHEET)) - - -class ColorDict: - - """A dict aimed at Qt stylesheet colors.""" - - def __init__(self, config): - self._config = config - - def __getitem__(self, key): - """Override dict __getitem__. - - Args: - key: The key to get from the dict. - - Return: - If a value wasn't found, return an empty string. - (Color not defined, so no output in the stylesheet) - - else, return the plain value. - """ - val = self._config.get('colors', key) - if isinstance(val, QColor): - # This could happen when accidentally declaring something as - # QtColor instead of Color in the config, and it'd go unnoticed as - # the CSS is invalid then. - raise TypeError("QColor passed to ColorDict!") - else: - return val diff --git a/qutebrowser/html/settings.html b/qutebrowser/html/settings.html index 50e1e00d9..cc872967f 100644 --- a/qutebrowser/html/settings.html +++ b/qutebrowser/html/settings.html @@ -22,6 +22,7 @@ th pre { color: grey; text-align: left; }

    {{ title }}

    + {% for section in config.DATA %} {% for d, e in config.DATA.get(section).items() %} diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index a93295e41..122b430f6 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -41,25 +41,25 @@ class Message(QLabel): """ if level == usertypes.MessageLevel.error: stylesheet += """ - background-color: {{ color['messages.bg.error'] }}; - color: {{ color['messages.fg.error'] }}; - font: {{ font['messages.error'] }}; - border-bottom: 1px solid {{ color['messages.border.error'] }}; + background-color: {{ conf.colors.messages.error.bg }}; + color: {{ conf.colors.messages.error.fg }}; + font: {{ conf.fonts.messages.error }}; + border-bottom: 1px solid {{ conf.colors.messages.error.border }}; """ elif level == usertypes.MessageLevel.warning: stylesheet += """ - background-color: {{ color['messages.bg.warning'] }}; - color: {{ color['messages.fg.warning'] }}; - font: {{ font['messages.warning'] }}; + background-color: {{ conf.colors.messages.warning.bg }}; + color: {{ conf.colors.messages.warning.fg }}; + font: {{ conf.fonts.messages.warning }}; border-bottom: - 1px solid {{ color['messages.border.warning'] }}; + 1px solid {{ conf.colors.messages.warning.border }}; """ elif level == usertypes.MessageLevel.info: stylesheet += """ - background-color: {{ color['messages.bg.info'] }}; - color: {{ color['messages.fg.info'] }}; - font: {{ font['messages.info'] }}; - border-bottom: 1px solid {{ color['messages.border.info'] }} + background-color: {{ conf.colors.messages.info.bg }}; + color: {{ conf.colors.messages.info.fg }}; + font: {{ conf.fonts.messages.info }}; + border-bottom: 1px solid {{ conf.colors.messages.info.border }} """ else: # pragma: no cover raise ValueError("Invalid level {!r}".format(level)) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 3c6a4aba2..d6deaa8e7 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -234,27 +234,27 @@ class PromptContainer(QWidget): STYLESHEET = """ QWidget#PromptContainer { - {% if config.val.statusbar.position == 'top' %} - border-bottom-left-radius: {{ config.val.prompt.radius }}px; - border-bottom-right-radius: {{ config.val.prompt.radius }}px; + {% if conf.statusbar.position == 'top' %} + border-bottom-left-radius: {{ conf.prompt.radius }}px; + border-bottom-right-radius: {{ conf.prompt.radius }}px; {% else %} - border-top-left-radius: {{ config.val.prompt.radius }}px; - border-top-right-radius: {{ config.val.prompt.radius }}px; + border-top-left-radius: {{ conf.prompt.radius }}px; + border-top-right-radius: {{ conf.prompt.radius }}px; {% endif %} } QWidget { - font: {{ font['prompts'] }}; - color: {{ color['prompts.fg'] }}; - background-color: {{ color['prompts.bg'] }}; + font: {{ conf.fonts.prompts }}; + color: {{ conf.colors.prompts.fg }}; + background-color: {{ conf.colors.prompts.bg }}; } QTreeView { - selection-background-color: {{ color['prompts.selected.bg'] }}; + selection-background-color: {{ conf.colors.prompts.selected.bg }}; } QTreeView::item:selected, QTreeView::item:selected:hover { - background-color: {{ color['prompts.selected.bg'] }}; + background-color: {{ conf.colors.prompts.selected.bg }}; } """ update_geometry = pyqtSignal() diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 29398bf07..d89a1ded1 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -81,21 +81,21 @@ class ColorFlags: def _generate_stylesheet(): flags = [ - ('private', 'statusbar.{}.private'), - ('caret', 'statusbar.{}.caret'), - ('caret-selection', 'statusbar.{}.caret-selection'), - ('prompt', 'prompts.{}'), - ('insert', 'statusbar.{}.insert'), - ('command', 'statusbar.{}.command'), - ('private-command', 'statusbar.{}.command.private'), + ('private', 'statusbar.private'), + ('caret', 'statusbar.caret'), + ('caret-selection', 'statusbar.caret.selection'), + ('prompt', 'prompts'), + ('insert', 'statusbar.insert'), + ('command', 'statusbar.command'), + ('private-command', 'statusbar.command.private'), ] stylesheet = """ QWidget#StatusBar, QWidget#StatusBar QLabel, QWidget#StatusBar QLineEdit { - font: {{ font['statusbar'] }}; - background-color: {{ color['statusbar.bg'] }}; - color: {{ color['statusbar.fg'] }}; + font: {{ conf.fonts.statusbar }}; + background-color: {{ conf.colors.statusbar.normal.bg }}; + color: {{ conf.colors.statusbar.normal.fg }}; } """ for flag, option in flags: @@ -103,11 +103,11 @@ def _generate_stylesheet(): QWidget#StatusBar[color_flags~="%s"], QWidget#StatusBar[color_flags~="%s"] QLabel, QWidget#StatusBar[color_flags~="%s"] QLineEdit { - color: {{ color['%s'] }}; - background-color: {{ color['%s'] }}; + color: {{ conf.colors.%s }}; + background-color: {{ conf.colors.%s }}; } """ % (flag, flag, flag, # flake8: disable=S001 - option.format('fg'), option.format('bg')) + option + '.fg', option + '.bg') return stylesheet diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py index a2f192732..f0800a07d 100644 --- a/qutebrowser/mainwindow/statusbar/progress.py +++ b/qutebrowser/mainwindow/statusbar/progress.py @@ -35,11 +35,11 @@ class Progress(QProgressBar): border-radius: 0px; border: 2px solid transparent; background-color: transparent; - font: {{ font['statusbar'] }}; + font: {{ conf.fonts.statusbar }}; } QProgressBar::chunk { - background-color: {{ color['statusbar.progress.bg'] }}; + background-color: {{ conf.colors.statusbar.progress.bg }}; } """ diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index f83fdef1f..87f226c0d 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -53,27 +53,27 @@ class UrlText(textbase.TextBase): STYLESHEET = """ QLabel#UrlText[urltype="normal"] { - color: {{ color['statusbar.url.fg'] }}; + color: {{ conf.colors.statusbar.url.fg }}; } QLabel#UrlText[urltype="success"] { - color: {{ color['statusbar.url.fg.success'] }}; + color: {{ conf.colors.statusbar.url.success.http.fg }}; } QLabel#UrlText[urltype="success_https"] { - color: {{ color['statusbar.url.fg.success.https'] }}; + color: {{ conf.colors.statusbar.url.success.https.fg }}; } QLabel#UrlText[urltype="error"] { - color: {{ color['statusbar.url.fg.error'] }}; + color: {{ conf.colors.statusbar.url.error.fg }}; } QLabel#UrlText[urltype="warn"] { - color: {{ color['statusbar.url.fg.warn'] }}; + color: {{ conf.colors.statusbar.url.warn.fg }}; } QLabel#UrlText[urltype="hover"] { - color: {{ color['statusbar.url.fg.hover'] }}; + color: {{ conf.colors.statusbar.url.hover.fg }}; } """ diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 0c8ce2d49..c5a4f5e68 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -47,11 +47,11 @@ class KeyHintView(QLabel): STYLESHEET = """ QLabel { - font: {{ font['keyhint'] }}; - color: {{ color['keyhint.fg'] }}; - background-color: {{ color['keyhint.bg'] }}; + font: {{ conf.fonts.keyhint }}; + color: {{ conf.colors.keyhint.fg }}; + background-color: {{ conf.colors.keyhint.bg }}; padding: 6px; - {% if config.val.statusbar.position == 'top' %} + {% if conf.statusbar.position == 'top' %} border-bottom-right-radius: 6px; {% else %} border-top-right-radius: 6px; @@ -109,7 +109,7 @@ class KeyHintView(QLabel): # delay so a quickly typed keychain doesn't display hints self._show_timer.setInterval(config.val.keyhint.delay) self._show_timer.start() - suffix_color = html.escape(config.val.colors.keyhint.fg.suffix) + suffix_color = html.escape(config.val.colors.keyhint.suffix.fg) text = '' for key, cmd in bindings: From 298553d48d21e0c15d8a0b9836d461e0b17b4132 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 14:44:20 +0200 Subject: [PATCH 029/516] Fix QssColor --- qutebrowser/config/configtypes.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index d114904a3..e90b31911 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -690,23 +690,26 @@ class QtColor(BaseType): raise configexc.ValidationError(value, "must be a valid color") -class QssColor(QtColor): +class QssColor(BaseType): """Color used in a Qt stylesheet.""" def from_py(self, value): - functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient', - 'qradialgradient', 'qconicalgradient'] self._basic_validation(value, pytype=str) if not value: return None + functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient', + 'qradialgradient', 'qconicalgradient'] if (any(value.startswith(func + '(') for func in functions) and value.endswith(')')): # QColor doesn't handle these return value - return super().from_py(value) + if not QColor.isValidColor(value): + raise configexc.ValidationError(value, "must be a valid color") + + return value class Font(BaseType): From e6275ab561b5273b5973731d90bd6f6ecfde89b4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 14:47:04 +0200 Subject: [PATCH 030/516] Fix startpage --- qutebrowser/app.py | 2 +- qutebrowser/browser/commands.py | 2 +- qutebrowser/mainwindow/tabbedbrowser.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 70a4c046f..dcc2620f0 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -310,7 +310,7 @@ def _open_startpage(win_id=None): window=cur_win_id) if tabbed_browser.count() == 0: log.init.debug("Opening startpage") - for urlstr in config.val.startpage: + for urlstr in config.val.start_page: try: url = urlutils.fuzzy_url(urlstr, do_search=False) except urlutils.InvalidUrlError as e: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 93435f385..6c42dcd6e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1192,7 +1192,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') def home(self): """Open main startpage in current tab.""" - self.openurl(config.val.startpage[0]) + self.openurl(config.val.start_page[0]) def _run_userscript(self, cmd, *args, verbose=False): """Run a userscript given as argument. diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index bfc402e3b..515ec547d 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -257,7 +257,7 @@ class TabbedBrowser(tabwidget.TabWidget): elif last_close == 'blank': self.openurl(QUrl('about:blank'), newtab=True) elif last_close == 'startpage': - url = QUrl(config.val.startpage[0]) + url = QUrl(config.val.start_page[0]) self.openurl(url, newtab=True) elif last_close == 'default-page': url = config.val.default_page @@ -323,7 +323,7 @@ class TabbedBrowser(tabwidget.TabWidget): no_history = len(self.widget(0).history) == 1 urls = { 'blank': QUrl('about:blank'), - 'startpage': QUrl(config.val.startpage[0]), + 'startpage': QUrl(config.val.start_page[0]), 'default_page': config.val.default_page, } first_tab_url = self.widget(0).url() From 500ad8b00f371b6530c5a6780051db28efb51142 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 14:49:15 +0200 Subject: [PATCH 031/516] Use strings for Perc configtypes --- qutebrowser/config/configtypes.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index e90b31911..c876b64f3 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -549,24 +549,19 @@ class Perc(_Numeric): """A percentage, as a string ending with %.""" - def from_str(self, value): + def from_py(self, value): + self._basic_validation(value, pytype=str) if not value: return None - elif not value.endswith('%'): - raise configexc.ValidationError(value, "does not end with %") + if not value.endswith('%'): + raise configexc.ValidationError(value, "does not end with %") try: floatval = float(value[:-1]) except ValueError: raise configexc.ValidationError(value, "must be a percentage!") - return self.from_py(floatval) - - def from_py(self, value): - self._basic_validation(value, pytype=float) - if not value: - return None - self._validate_bounds(value, suffix='%') - return value + self._validate_bounds(floatval, suffix='%') + return floatval class PercOrInt(_Numeric): From 269e9d69e072a1dd69961180b8c4d95268c25191 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 14:51:32 +0200 Subject: [PATCH 032/516] Improve typechecking message --- qutebrowser/config/configtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index c876b64f3..79ba4a18f 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -122,8 +122,8 @@ class BaseType: if (value is not None and pytype is not None and not isinstance(value, pytype)): raise configexc.ValidationError( - value, "expected a value of type {} but got {}".format( - pytype, type(value))) + value, "expected a value of type {} but got {}.".format( + pytype.__name__, type(value).__name__)) if isinstance(value, str): if not value and not self.none_ok: From fcc0b3e8c0c7c2f1c9aa8158135048b45ef01271 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 14:54:22 +0200 Subject: [PATCH 033/516] Fix tab indicator width --- qutebrowser/mainwindow/tabwidget.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 9275eebbb..8db31f122 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -466,10 +466,9 @@ class TabBar(QTabBar): icon_size = icon.actualSize(QSize(extent, extent)) padding_h += self.style().pixelMetric( PixelMetrics.icon_padding, None, self) - indicator_width = config.val.tabs.indicator_width height = self.fontMetrics().height() + padding_v width = (self.fontMetrics().width('\u2026') + icon_size.width() + - padding_h + indicator_width) + padding_h + config.val.tabs.width.indicator) return QSize(width, height) def tabSizeHint(self, index): @@ -505,19 +504,30 @@ class TabBar(QTabBar): # get scroll buttons as soon as needed. size = minimum_size else: - tab_width_pinned_conf = config.val.tabs.pinned_width - try: pinned = self.tab_data(index, 'pinned') except KeyError: pinned = False no_pinned_count = self.count() - self.pinned_count - pinned_width = tab_width_pinned_conf * self.pinned_count + pinned_width = config.val.tabs.width.pinned * self.pinned_count no_pinned_width = self.width() - pinned_width if pinned: - width = tab_width_pinned_conf + size = QSize(config.val.tabs.width.pinned, height) + qtutils.ensure_valid(size) + return size + + # If we *do* have enough space, tabs should occupy the whole window + # width. If there are pinned tabs their size will be subtracted + # from the total window width. + # During shutdown the self.count goes down, + # but the self.pinned_count not - this generates some odd behavior. + # To avoid this we compare self.count against self.pinned_count. + if self.pinned_count > 0 and self.count() > self.pinned_count: + pinned_width = config.val.tabs.width.pinned * self.pinned_count + no_pinned_width = self.width() - pinned_width + width = no_pinned_width / (self.count() - self.pinned_count) else: # If we *do* have enough space, tabs should occupy the whole @@ -785,7 +795,7 @@ class TabBarStyle(QCommonStyle): text_rect.adjust(padding.left, padding.top, -padding.right, -padding.bottom) - indicator_width = config.val.tabs.indicator_width + indicator_width = config.val.tabs.width.indicator if indicator_width == 0: indicator_rect = QRect() else: From b9aa5d0e4e284029e2ffbc2b77554eccf3eadf13 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 15:09:38 +0200 Subject: [PATCH 034/516] Fix valid_values initing from configdata --- qutebrowser/config/configdata.py | 3 +++ qutebrowser/config/configtypes.py | 20 +++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index eea7ec01d..0ef0d1e25 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -553,6 +553,9 @@ def _parse_yaml_type(name, node): # -> create the type object and pass arguments type_name = node.pop('name') kwargs = node + valid_values = kwargs.get('valid_values', None) + if valid_values is not None: + kwargs['valid_values'] = configtypes.ValidValues(*valid_values) else: _raise_invalid_node(name, 'type', node) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 79ba4a18f..88e474a4a 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -54,19 +54,25 @@ class ValidValues: descriptions: A dict with value/desc mappings. """ - def __init__(self, *vals): - if not vals: + def __init__(self, *values): + if not values: raise ValueError("ValidValues with no values makes no sense!") self.descriptions = {} self.values = [] - for v in vals: - if isinstance(v, str): + for value in values: + if isinstance(value, str): # Value without description - self.values.append(v) + self.values.append(value) + elif isinstance(value, dict): + # List of dicts from configdata.yml + assert len(value) == 1, value + value, desc = list(value.items())[0] + self.values.append(value) + self.descriptions[value] = desc else: # (value, description) tuple - self.values.append(v[0]) - self.descriptions[v[0]] = v[1] + self.values.append(value[0]) + self.descriptions[value[0]] = value[1] def __contains__(self, val): return val in self.values From 1f508d9d8fb4598635349c78d06e8708480c0ada Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 15:10:34 +0200 Subject: [PATCH 035/516] Fix config getters --- qutebrowser/browser/webengine/interceptor.py | 2 +- qutebrowser/mainwindow/tabwidget.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 71744f5d7..e041968fd 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -63,6 +63,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): for header, value in shared.custom_headers(): info.setHttpHeader(header, value) - user_agent = config.val.conent.user_agent + user_agent = config.val.content.user_agent if user_agent is not None: info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 8db31f122..f3a72bb10 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -140,8 +140,8 @@ class TabWidget(QTabWidget): fields['title'] = fields['title'].replace('&', '&&') fields['index'] = idx + 1 - fmt = config.val.tabs.title_format - fmt_pinned = config.val.tabs.title_format_pinned + fmt = config.val.tabs.title.format + fmt_pinned = config.val.tabs.title.format_pinned if tab.data.pinned: title = '' if fmt_pinned is None else fmt_pinned.format(**fields) @@ -714,7 +714,7 @@ class TabBarStyle(QCommonStyle): elif element == QStyle.CE_TabBarTabLabel: if not opt.icon.isNull() and layouts.icon.isValid(): self._draw_icon(layouts, opt, p) - alignment = (config.val.tabs.title_alignment | + alignment = (config.val.tabs.title.alignment | Qt.AlignVCenter | Qt.TextHideMnemonic) self._style.drawItemText(p, layouts.text, alignment, opt.palette, opt.state & QStyle.State_Enabled, From 8c1b5f0581feb3647842e5a9bf34e25bdb221e09 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 15:18:10 +0200 Subject: [PATCH 036/516] Fix up background_tabs, favicon_show and some custom config magic --- qutebrowser/browser/commands.py | 2 +- qutebrowser/browser/hints.py | 5 ++--- qutebrowser/browser/webelem.py | 2 +- qutebrowser/browser/webengine/webview.py | 10 +++++----- qutebrowser/browser/webkit/webview.py | 6 +++--- qutebrowser/mainwindow/tabbedbrowser.py | 6 +++--- qutebrowser/mainwindow/tabwidget.py | 22 ++++++---------------- 7 files changed, 21 insertions(+), 32 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 6c42dcd6e..3bd5569ee 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -507,7 +507,7 @@ class CommandDispatcher: idx = new_tabbed_browser.indexOf(newtab) new_tabbed_browser.set_page_title(idx, cur_title) - if config.val.tabs.show_favicons: + if config.val.tabs.favicons.show: new_tabbed_browser.setTabIcon(idx, curtab.icon()) if config.val.tabs.tabs_are_windows: new_tabbed_browser.window().setWindowIcon(curtab.icon()) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 625f9097f..10686c0fd 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -203,7 +203,7 @@ class HintActions: Target.window: usertypes.ClickTarget.window, Target.hover: usertypes.ClickTarget.normal, } - if config.val.tabs.background_tabs: + if config.val.tabs.background: target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg else: target_mapping[Target.tab] = usertypes.ClickTarget.tab @@ -684,8 +684,7 @@ class HintManager(QObject): Target.hover, Target.userscript, Target.spawn, Target.download, Target.normal, Target.current]: pass - elif (target == Target.tab and - config.val.tabs.background_tabs): + elif target == Target.tab and config.val.tabs.background: pass else: name = target.name.replace('_', '-') diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 401a3bb93..66d86e273 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -329,7 +329,7 @@ class AbstractWebElement(collections.abc.MutableMapping): usertypes.ClickTarget.tab: Qt.ControlModifier, usertypes.ClickTarget.tab_bg: Qt.ControlModifier, } - if config.val.tabs.background_tabs: + if config.val.tabs.background: modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier else: modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index f8cefc300..f7221426c 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -80,10 +80,10 @@ class WebEngineView(QWebEngineView): The new QWebEngineView object. """ debug_type = debug.qenum_key(QWebEnginePage, wintype) - background_tabs = config.val.tabs.background_tabs + background = config.val.tabs.background - log.webview.debug("createWindow with type {}, background_tabs " - "{}".format(debug_type, background_tabs)) + log.webview.debug("createWindow with type {}, background {}".format( + debug_type, background)) if wintype == QWebEnginePage.WebBrowserWindow: # Shift-Alt-Click @@ -95,13 +95,13 @@ class WebEngineView(QWebEngineView): elif wintype == QWebEnginePage.WebBrowserTab: # Middle-click / Ctrl-Click with Shift # FIXME:qtwebengine this also affects target=_blank links... - if background_tabs: + if background: target = usertypes.ClickTarget.tab else: target = usertypes.ClickTarget.tab_bg elif wintype == QWebEnginePage.WebBrowserBackgroundTab: # Middle-click / Ctrl-Click - if background_tabs: + if background: target = usertypes.ClickTarget.tab_bg else: target = usertypes.ClickTarget.tab diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index cbdfb1854..682f8b55f 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -285,10 +285,10 @@ class WebView(QWebView): This is implemented here as we don't need it for QtWebEngine. """ if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier: - background_tabs = config.val.tabs.background_tabs + background = config.val.tabs.background if e.modifiers() & Qt.ShiftModifier: - background_tabs = not background_tabs - if background_tabs: + background = not background + if background: target = usertypes.ClickTarget.tab_bg else: target = usertypes.ClickTarget.tab diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 515ec547d..81025570c 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -430,7 +430,7 @@ class TabbedBrowser(tabwidget.TabWidget): tab.openurl(url) if background is None: - background = config.val.tabs.background_tabs + background = config.val.tabs.background if background: # Make sure the background tab has the correct initial size. # With a foreground tab, it's going to be resized correctly by the @@ -516,7 +516,7 @@ class TabbedBrowser(tabwidget.TabWidget): else: self.setTabIcon(idx, QIcon()) if (config.val.tabs.tabs_are_windows and - config.val.tabs.show_favicons): + config.val.tabs.favicons.show): self.window().setWindowIcon(self.default_window_icon) if idx == self.currentIndex(): self.update_window_title() @@ -580,7 +580,7 @@ class TabbedBrowser(tabwidget.TabWidget): tab: The WebView where the title was changed. icon: The new icon """ - if not config.val.tabs.show_favicons: + if not config.val.tabs.favicons.show: return try: idx = self._tab_index(tab) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index f3a72bb10..f5301e18a 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -562,23 +562,13 @@ class TabBar(QTabBar): tab = QStyleOptionTab() self.initStyleOption(tab, idx) - bg_parts = ['tabs', 'bg'] - fg_parts = ['tabs', 'fg'] + setting = config.val.colors.tabs if idx == selected: - bg_parts.append('selected') - fg_parts.append('selected') + setting = setting.selected + setting = setting.odd if idx % 2 else setting.even - if idx % 2: - bg_parts.append('odd') - fg_parts.append('odd') - else: - bg_parts.append('even') - fg_parts.append('even') - - bg_color = config.get('colors', '.'.join(bg_parts)) - fg_color = config.get('colors', '.'.join(fg_parts)) - tab.palette.setColor(QPalette.Window, bg_color) - tab.palette.setColor(QPalette.WindowText, fg_color) + tab.palette.setColor(QPalette.Window, setting.bg) + tab.palette.setColor(QPalette.WindowText, setting.fg) indicator_color = self.tab_indicator_color(idx) tab.palette.setColor(QPalette.Base, indicator_color) @@ -841,7 +831,7 @@ class TabBarStyle(QCommonStyle): position = config.val.tabs.position if (opt.icon.isNull() and position in [QTabWidget.East, QTabWidget.West] and - config.val.tabs.show_favicons): + config.val.tabs.favicons.show): tab_icon_size = icon_size else: actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state) From c8c9536beb38a7af0ce7c39027087f4eb640308f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 15:21:40 +0200 Subject: [PATCH 037/516] Fix completion stuff --- qutebrowser/browser/browsertab.py | 2 +- qutebrowser/browser/mouse.py | 4 ++-- qutebrowser/browser/webelem.py | 4 ++-- qutebrowser/completion/completer.py | 2 +- qutebrowser/completion/completiondelegate.py | 6 +++--- qutebrowser/completion/completionwidget.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index c3ac426db..d33c7b046 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -704,7 +704,7 @@ class AbstractTab(QWidget): def _handle_auto_insert_mode(self, ok): """Handle auto-insert-mode after loading finished.""" - if not config.val.input.auto_insert_mode or not ok: + if not config.val.input.insert_mode.auto_focused or not ok: return cur_mode = self._mode_manager.mode diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 18069a003..51de41105 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -157,7 +157,7 @@ class MouseEventFilter(QObject): 'click', only_if_normal=True) else: log.mouse.debug("Clicked non-editable element!") - if config.val.input.auto_leave_insert_mode: + if config.val.input.insert_mode.auto_leave: modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, 'click', maybe=True) @@ -179,7 +179,7 @@ class MouseEventFilter(QObject): 'click-delayed', only_if_normal=True) else: log.mouse.debug("Clicked non-editable element (delayed)!") - if config.val.input.auto_leave_insert_mode: + if config.val.input.insert_mode.auto_leave: modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, 'click-delayed', maybe=True) diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 66d86e273..df3001b52 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -182,7 +182,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # at least a classid attribute. Oh, and let's hope images/... # DON'T have a classid attribute. HTML sucks. log.webelem.debug(" clicked.".format(objtype)) - return config.val.input.insert_mode_on_plugins + return config.val.input.insert_mode.plugins else: # Image/Audio/... return False @@ -247,7 +247,7 @@ class AbstractWebElement(collections.abc.MutableMapping): return self.is_writable() elif tag in ['embed', 'applet']: # Flash/Java/... - return config.val.input.insert_mode_on_plugins and not strict + return config.val.input.insert_mode.plugins and not strict elif tag == 'object': return self._is_editable_object() and not strict elif tag in ['div', 'pre']: diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 38302fa5e..7824de075 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -194,7 +194,7 @@ class Completer(QObject): if maxsplit is None: text = self._quote(text) model = self._model() - if model.count() == 1 and config.val.completion.quick_complete: + if model.count() == 1 and config.val.completion.quick: # If we only have one item, we want to apply it immediately # and go on to the next part. self._change_completed_part(text, before, after, immediate=True) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index 343c7d359..af4141d4d 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -149,11 +149,11 @@ class CompletionItemDelegate(QStyledItemDelegate): self._painter.save() if self._opt.state & QStyle.State_Selected: - color = config.val.completion.item.selected.fg + color = config.val.colors.completion.item.selected.fg elif not self._opt.state & QStyle.State_Enabled: - color = config.val.completion.category.fg + color = config.val.colors.completion.category.fg else: - color = config.val.completion.fg + color = config.val.colors.completion.fg self._painter.setPen(color) ctx = QAbstractTextDocumentLayout.PaintContext() diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index eedd21e94..bd38e065a 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -257,7 +257,7 @@ class CompletionView(QTreeView): count = self.model().count() if count == 0: self.hide() - elif count == 1 and config.val.completion.quick_complete: + elif count == 1 and config.val.completion.quick: self.hide() elif config.val.completion.show == 'auto': self.show() From 4b4acc5f5ad54efe5731a6a14b5af9d00c52daba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 15:54:36 +0200 Subject: [PATCH 038/516] Minor config fixes --- qutebrowser/app.py | 2 +- qutebrowser/browser/webkit/webkitsettings.py | 2 +- qutebrowser/config/configdata.yml | 2 +- qutebrowser/config/configtypes.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index dcc2620f0..df3168e88 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -674,7 +674,7 @@ class Quitter: if session is not None: session_manager.save(session, last_window=last_window, load_next_time=True) - elif config.val.save_session: + elif config.val.auto_save.session: session_manager.save(sessions.default, last_window=last_window, load_next_time=True) diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index a51c7384f..769d692bc 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -175,7 +175,7 @@ MAPPINGS = { CookiePolicy(), 'content.dns_prefetch': Attribute(QWebSettings.DnsPrefetchEnabled), - 'content.frame-flattening': + 'content.frame_flattening': Attribute(QWebSettings.FrameFlatteningEnabled), 'content.offline_web_application_cache': Attribute(QWebSettings.OfflineWebApplicationCacheEnabled), diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 0c4c6924c..f7c3aa7d7 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -262,7 +262,7 @@ messages.unfocused: confirm_quit: type: ConfirmQuit - default: never + default: [never] desc: Whether to confirm quitting the application. zoom.text_only: diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 88e474a4a..ee8713b8c 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -397,7 +397,7 @@ class FlagList(List): _show_valtype = False def __init__(self, none_ok=False, valid_values=None): - super().__init__(BaseType(), none_ok) + super().__init__(String(), none_ok) self.valtype.valid_values = valid_values def _check_duplicates(self, values): From 1022b7ea32d1bd665613b7537a073bfb4d58a531 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 16:02:56 +0200 Subject: [PATCH 039/516] Make jinja templating more strict This ensures we actually know when an AttributeError happens. It also changes most external code to use the correct environment, rather than simply creating a jinja2.Template, which wouldn't use the more tightened environment. --- qutebrowser/browser/shared.py | 6 +-- qutebrowser/config/style.py | 5 +-- qutebrowser/mainwindow/mainwindow.py | 6 +-- qutebrowser/utils/jinja.py | 67 +++++++++++++++++----------- tests/unit/utils/test_jinja.py | 11 ++++- 5 files changed, 57 insertions(+), 38 deletions(-) diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 4ddc22786..1006907ea 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -21,10 +21,8 @@ import html -import jinja2 - from qutebrowser.config import config -from qutebrowser.utils import usertypes, message, log, objreg +from qutebrowser.utils import usertypes, message, log, objreg, jinja class CallSuper(Exception): @@ -137,7 +135,7 @@ def ignore_certificate_errors(url, errors, abort_on): assert error.is_overridable(), repr(error) if ssl_strict == 'ask': - err_template = jinja2.Template(""" + err_template = jinja.environment.from_string(""" Errors while loading {{url.toDisplayString()}}:
      {% for err in errors %} diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index e36aebcbf..e6d5333e5 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -22,12 +22,11 @@ import functools import collections -import jinja2 import sip from PyQt5.QtGui import QColor from qutebrowser.config import config -from qutebrowser.utils import log, objreg +from qutebrowser.utils import log, objreg, jinja @functools.lru_cache(maxsize=16) @@ -40,7 +39,7 @@ def get_stylesheet(template_str): Return: The formatted template as string. """ - template = jinja2.Template(template_str) + template = jinja.environment.from_string(template_str) return template.render(conf=config.val) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 672696a2a..2483a37b0 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -24,13 +24,13 @@ import base64 import itertools import functools -import jinja2 from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from qutebrowser.commands import runners, cmdutils from qutebrowser.config import config -from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils +from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, + jinja) from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt from qutebrowser.mainwindow.statusbar import bar from qutebrowser.completion import completionwidget, completer @@ -552,7 +552,7 @@ class MainWindow(QWidget): "download is" if download_count == 1 else "downloads are")) # Process all quit messages that user must confirm if quit_texts or 'always' in config.val.confirm_quit: - msg = jinja2.Template(""" + msg = jinja.environment.from_string("""
        {% for text in quit_texts %}
      • {{text}}
      • diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index 731dec8e4..e2eab4542 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -76,40 +76,56 @@ class Loader(jinja2.BaseLoader): return source, path, lambda: True -def _guess_autoescape(template_name): - """Turn auto-escape on/off based on the file type. +class Environment(jinja2.Environment): - Based on http://jinja.pocoo.org/docs/dev/api/#autoescaping - """ - if template_name is None or '.' not in template_name: - return False - ext = template_name.rsplit('.', 1)[1] - return ext in ['html', 'htm', 'xml'] + def __init__(self): + super().__init__(loader=Loader('html'), + autoescape=self._guess_autoescape, + undefined=jinja2.StrictUndefined) + self.globals['resource_url'] = self._resource_url + self.globals['file_url'] = urlutils.file_url + self.globals['data_url'] = self._data_url + def _guess_autoescape(self, template_name): + """Turn auto-escape on/off based on the file type. -def resource_url(path): - """Load images from a relative path (to qutebrowser). + Based on http://jinja.pocoo.org/docs/dev/api/#autoescaping + """ + if template_name is None or '.' not in template_name: + return False + ext = template_name.rsplit('.', 1)[1] + return ext in ['html', 'htm', 'xml'] - Arguments: - path: The relative path to the image - """ - image = utils.resource_filename(path) - return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded) + def _resource_url(self, path): + """Load images from a relative path (to qutebrowser). + Arguments: + path: The relative path to the image + """ + image = utils.resource_filename(path) + return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded) -def data_url(path): - """Get a data: url for the broken qutebrowser logo.""" - data = utils.read_file(path, binary=True) - filename = utils.resource_filename(path) - mimetype = mimetypes.guess_type(filename) - assert mimetype is not None, path - return urlutils.data_url(mimetype[0], data).toString() + def _data_url(self, path): + """Get a data: url for the broken qutebrowser logo.""" + data = utils.read_file(path, binary=True) + filename = utils.resource_filename(path) + mimetype = mimetypes.guess_type(filename) + assert mimetype is not None, path + return urlutils.data_url(mimetype[0], data).toString() + + def getattr(self, obj, attribute): + """Override jinja's getattr() to be less clever. + + This means it doesn't fall back to __getitem__, and it doesn't hide + AttributeError. + """ + return getattr(obj, attribute) def render(template, **kwargs): """Render the given template and pass the given arguments to it.""" try: - return _env.get_template(template).render(**kwargs) + return environment.get_template(template).render(**kwargs) except jinja2.exceptions.UndefinedError: log.misc.exception("UndefinedError while rendering " + template) err_path = os.path.join('html', 'undef_error.html') @@ -118,7 +134,4 @@ def render(template, **kwargs): return err_template.format(pagename=template, traceback=tb) -_env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape) -_env.globals['resource_url'] = resource_url -_env.globals['file_url'] = urlutils.file_url -_env.globals['data_url'] = data_url +environment = Environment() diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py index dc22cb0d7..40f9c12bf 100644 --- a/tests/unit/utils/test_jinja.py +++ b/tests/unit/utils/test_jinja.py @@ -55,6 +55,9 @@ def patch_read_file(monkeypatch): elif path == os.path.join('html', 'undef_error.html'): assert not binary return real_read_file(path) + elif path == os.path.join('html', 'attributeerror.html'): + assert not binary + return """{{ obj.foobar }}""" else: raise IOError("Invalid path {}!".format(path)) @@ -137,6 +140,12 @@ def test_undefined_function(caplog): assert caplog.records[0].msg == "UndefinedError while rendering undef.html" +def test_attribute_error(): + """Make sure accessing an unknown attribute fails.""" + with pytest.raises(AttributeError): + jinja.render('attributeerror.html', obj=object()) + + @pytest.mark.parametrize('name, expected', [ (None, False), ('foo', False), @@ -147,4 +156,4 @@ def test_undefined_function(caplog): ('foo.bar.html', True), ]) def test_autoescape(name, expected): - assert jinja._guess_autoescape(name) == expected + assert jinja.environment._guess_autoescape(name) == expected From c25022f5496f74f5e2a197c89a9eff7666098afd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 16:07:12 +0200 Subject: [PATCH 040/516] Fix LimitLineParser --- qutebrowser/config/config.py | 2 +- qutebrowser/misc/lineparser.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index e223a3b14..a51fda016 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -158,7 +158,7 @@ def _init_misc(): from qutebrowser.misc import lineparser command_history = lineparser.LimitLineParser( standarddir.data(), 'cmd-history', - limit=('completion', 'cmd-history-max-items'), + limit='completion.cmd_history_max_items', parent=objreg.get('config')) objreg.register('command-history', command_history) save_manager.add_saveable('command-history', command_history.save, diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index ea9d100b7..f137b91f3 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -263,8 +263,7 @@ class LimitLineParser(LineParser): """A LineParser with a limited count of lines. Attributes: - _limit: The config section/option used to limit the maximum number of - lines. + _limit: The config option used to limit the maximum number of lines. """ def __init__(self, configdir, fname, *, limit, binary=False, parent=None): @@ -273,7 +272,7 @@ class LimitLineParser(LineParser): Args: configdir: Directory to read the config from, or None. fname: Filename of the config file. - limit: Config tuple (section, option) which contains a limit. + limit: Config option which contains a limit. binary: Whether to open the file in binary mode. """ super().__init__(configdir, fname, binary=binary, parent=parent) @@ -286,20 +285,20 @@ class LimitLineParser(LineParser): configdir=self._configdir, fname=self._fname, limit=self._limit, binary=self._binary) - @pyqtSlot(str, str) - def cleanup_file(self, section, option): + @pyqtSlot(str) + def cleanup_file(self, option): """Delete the file if the limit was changed to 0.""" assert self._configfile is not None - if (section, option) != self._limit: + if option != self._limit: return - value = config.get(section, option) + value = config.get(option) if value == 0: if os.path.exists(self._configfile): os.remove(self._configfile) def save(self): """Save the config file.""" - limit = config.get(*self._limit) + limit = config.get(self._limit) if limit == 0: return do_save = self._prepare_save() From e828f5b81247481d95c87fe13ebe566a9478f0fc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 16:15:41 +0200 Subject: [PATCH 041/516] Fix most config changed handlers --- qutebrowser/browser/browsertab.py | 6 +++--- qutebrowser/completion/completionwidget.py | 8 +++----- qutebrowser/mainwindow/mainwindow.py | 12 +++++------- qutebrowser/mainwindow/statusbar/bar.py | 10 ++++------ qutebrowser/mainwindow/tabwidget.py | 11 +++++------ 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index d33c7b046..81cf190a0 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -260,9 +260,9 @@ class AbstractZoom(QObject): # self.destroyed.connect(functools.partial( # cfg.changed.disconnect, self.init_neighborlist)) - @pyqtSlot(str, str) - def _on_config_changed(self, section, option): - if section == 'ui' and option in ['zoom-levels', 'default-zoom']: + @pyqtSlot(str) + def _on_config_changed(self, option): + if option in ['zoom.levels', 'zoom.default']: if not self._default_zoom_changed: factor = float(config.val.zoom.default) / 100 self._set_factor_internal(factor) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index bd38e065a..62711aa5f 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -141,11 +141,9 @@ class CompletionView(QTreeView): def __repr__(self): return utils.get_repr(self) - @pyqtSlot(str, str) - def _on_config_changed(self, section, option): - if section != 'completion': - return - if option in ['height', 'shrink']: + @pyqtSlot(str) + def _on_config_changed(self, option): + if option in ['completion.height', 'completion.shrink']: self.update_geometry.emit() def _resize_columns(self): diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 2483a37b0..5df4aedef 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -323,16 +323,14 @@ class MainWindow(QWidget): def __repr__(self): return utils.get_repr(self) - @pyqtSlot(str, str) - def on_config_changed(self, section, option): + @pyqtSlot(str) + def on_config_changed(self, option): """Resize the completion if related config options changed.""" - if section != 'ui': - return - if option == 'statusbar-padding': + if option == 'statusbar.padding': self._update_overlay_geometries() - elif option == 'downloads-position': + elif option == 'downloads.position': self._add_widgets() - elif option == 'status-position': + elif option == 'statusbar.position': self._add_widgets() self._update_overlay_geometries() diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index d89a1ded1..dcabdc6ab 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -199,13 +199,11 @@ class StatusBar(QWidget): def __repr__(self): return utils.get_repr(self) - @pyqtSlot(str, str) - def _on_config_changed(self, section, option): - if section != 'ui': - return - if option == 'hide-statusbar': + @pyqtSlot(str) + def _on_config_changed(self, option): + if option == 'statusbar.hide': self.maybe_hide() - elif option == 'statusbar-pdading': + elif option == 'statusbar.padding': self._set_hbox_padding() @pyqtSlot() diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index f5301e18a..6034a3ce1 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -189,10 +189,9 @@ class TabWidget(QTabWidget): fields['scroll_pos'] = scroll_pos return fields - def update_tab_titles(self, section='tabs', option='title-format'): + def update_tab_titles(self, option='tabs.title.format'): """Update all texts.""" - if section == 'tabs' and option in ['title-format', - 'title-format-pinned']: + if option in ['tabs.title.format', 'tabs.title.format_pinned']: for idx in range(self.count()): self.update_tab_title(idx) @@ -426,10 +425,10 @@ class TabBar(QTabBar): p.setColor(QPalette.Window, config.val.colors.tabs.bar.bg) self.setPalette(p) - @pyqtSlot(str, str) - def on_tab_colors_changed(self, section, option): + @pyqtSlot(str) + def on_tab_colors_changed(self, option): """Set the tab colors.""" - if section == 'colors' and option.startswith('tabs.'): + if option.startswith('colors.tabs.'): self.update() def mousePressEvent(self, e): From 45ce7efc7191799941c73a26700b6e461b028fe4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 16:18:50 +0200 Subject: [PATCH 042/516] Adjust feature permissions --- qutebrowser/browser/shared.py | 2 +- qutebrowser/browser/webengine/webview.py | 9 ++++----- qutebrowser/browser/webkit/webpage.py | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 1006907ea..6caeafb07 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -171,7 +171,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on): Args: url: The URL the request was done for. - option: A (section, option) tuple for the option to check. + option: An option name to check. msg: A string like "show notifications" yes_action: A callable to call if the request was approved no_action: A callable to call if the request was denied diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index f7221426c..1a115c9de 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -148,11 +148,10 @@ class WebEnginePage(QWebEnginePage): def _on_feature_permission_requested(self, url, feature): """Ask the user for approval for geolocation/media/etc..""" options = { - QWebEnginePage.Geolocation: ('content', 'geolocation'), - QWebEnginePage.MediaAudioCapture: ('content', 'media-capture'), - QWebEnginePage.MediaVideoCapture: ('content', 'media-capture'), - QWebEnginePage.MediaAudioVideoCapture: - ('content', 'media-capture'), + QWebEnginePage.Geolocation: 'content.geolocation', + QWebEnginePage.MediaAudioCapture: 'content.media_capture', + QWebEnginePage.MediaVideoCapture: 'content.media_capture', + QWebEnginePage.MediaAudioVideoCapture: 'content.media_capture', } messages = { QWebEnginePage.Geolocation: 'access your location', diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index ac156d12f..10ff1ba8a 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -304,8 +304,8 @@ class BrowserPage(QWebPage): return options = { - QWebPage.Notifications: ('content', 'notifications'), - QWebPage.Geolocation: ('content', 'geolocation'), + QWebPage.Notifications: 'content.notifications', + QWebPage.Geolocation: 'content.geolocation', } messages = { QWebPage.Notifications: 'show notifications', From 3cee9cdcd7e4542f9e5c896e3e530dbc29cfcace Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2017 16:21:03 +0200 Subject: [PATCH 043/516] Fix JS logging --- qutebrowser/browser/webengine/webview.py | 3 +-- qutebrowser/browser/webkit/webpage.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 1a115c9de..bb160d1a2 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -276,8 +276,7 @@ class WebEnginePage(QWebEnginePage): def javaScriptConsoleMessage(self, level, msg, line, source): """Log javascript messages to qutebrowser's log.""" # FIXME:qtwebengine maybe unify this in the tab api somehow? - setting = config.val.log_javascript_console - if setting == 'none': + if config.val.content.javascript.log == 'none': return level_to_logger = { diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 10ff1ba8a..b7a8fe2a5 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -446,15 +446,14 @@ class BrowserPage(QWebPage): def javaScriptConsoleMessage(self, msg, line, source): """Override javaScriptConsoleMessage to use debug log.""" - log_javascript_console = config.get('general', - 'log-javascript-console') logstring = "[{}:{}] {}".format(source, line, msg) logmap = { 'debug': log.js.debug, 'info': log.js.info, 'none': lambda arg: None } - logmap[log_javascript_console](logstring) + logger = logmap[config.val.content.javascript.log] + logger(logstring) def acceptNavigationRequest(self, _frame: QWebFrame, From cc90cc68437fe4d0fcf4b8d2d0bd43bcc1c28f48 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 11:44:18 +0200 Subject: [PATCH 044/516] Initial pylint checker update --- .../dev/pylint_checkers/qute_pylint/config.py | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/scripts/dev/pylint_checkers/qute_pylint/config.py b/scripts/dev/pylint_checkers/qute_pylint/config.py index 60334f3fd..bfd8d6ba0 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/config.py +++ b/scripts/dev/pylint_checkers/qute_pylint/config.py @@ -29,6 +29,9 @@ from pylint import interfaces, checkers from pylint.checkers import utils +OPTIONS = None + + class ConfigChecker(checkers.BaseChecker): """Custom astroid checker for config calls.""" @@ -36,47 +39,36 @@ class ConfigChecker(checkers.BaseChecker): __implements__ = interfaces.IAstroidChecker name = 'config' msgs = { - 'E0000': ('%s is no valid config option.', # flake8: disable=S001 + 'E9998': ('%s is no valid config option.', # flake8: disable=S001 'bad-config-call', None), - 'E0001': ('old config call', # flake8: disable=S001 + 'E9999': ('old config call', # flake8: disable=S001 'old-config-call', None), } priority = -1 @utils.check_messages('bad-config-call') - def visit_call(self, node): - """Visit a Call node.""" - if hasattr(node, 'func'): - infer = utils.safe_infer(node.func) - if infer and infer.root().name == 'qutebrowser.config.config': - if getattr(node.func, 'attrname', None) in ['get', 'set']: - self._check_config(node) + def visit_attribute(self, node): + """Visit a getattr node.""" + # At the end of a config.val.foo.bar chain + if not isinstance(node.parent, astroid.Attribute): + # FIXME do some proper check for this... + node_str = node.as_string() + prefix = 'config.val.' + if node_str.startswith(prefix): + self._check_config(node, node_str[len(prefix):]) - def _check_config(self, node): - """Check that the arguments to config.get(...) are valid.""" - # try: - # sect_arg = utils.get_argument_from_call(node, position=0, - # keyword='sectname') - # opt_arg = utils.get_argument_from_call(node, position=1, - # keyword='optname') - # except utils.NoSuchArgumentError: - # return - # sect_arg = utils.safe_infer(sect_arg) - # opt_arg = utils.safe_infer(opt_arg) - # if not (isinstance(sect_arg, astroid.Const) and - # isinstance(opt_arg, astroid.Const)): - # return - # try: - # configdata.DATA[sect_arg.value][opt_arg.value] - # except KeyError: - # self.add_message('bad-config-call', node=node, - # args=(sect_arg.value, opt_arg.value)) - - self.add_message('old-config-call', node=node) + def _check_config(self, node, name): + """Check that we're accessing proper config options.""" + if name not in OPTIONS: + self.add_message('bad-config-call', node=node, args=name) def register(linter): """Register this checker.""" linter.register_checker(ConfigChecker(linter)) + global OPTIONS + yaml_file = os.path.join('qutebrowser', 'config', 'configdata.yml') + with open(yaml_file, 'r', encoding='utf-8') as f: + OPTIONS = list(yaml.load(f)) From d751539a25b402efad4efe2d583251b7e94be006 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 18:13:54 +0200 Subject: [PATCH 045/516] Add __eq__ and __repr__ for PACFetcher This makes it possible to use it in comparisons during tests easily. --- qutebrowser/browser/network/pac.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index 74a6c4296..c6619311c 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -252,6 +252,12 @@ class PACFetcher(QObject): self._pac = None self._error_message = None + def __eq__(self, other): + return self._pac_url == other._pac_url + + def __repr__(self): + return utils.get_repr(self, url=self._pac_url, constructor=True) + @pyqtSlot() def _finish(self): if self._reply.error() != QNetworkReply.NoError: From ce7597b3f68ad6cda03be2345fc10317a66de4c3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 18:14:40 +0200 Subject: [PATCH 046/516] Fix various configtypes issues found while writing tests --- qutebrowser/config/configtypes.py | 99 +++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 26 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index ee8713b8c..3fd1b1869 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -127,9 +127,13 @@ class BaseType: if (value is not None and pytype is not None and not isinstance(value, pytype)): + if isinstance(pytype, tuple): + expected = ' or '.join(typ.__name__ for typ in pytype) + else: + expected = pytype.__name__ raise configexc.ValidationError( value, "expected a value of type {} but got {}.".format( - pytype.__name__, type(value).__name__)) + expected, type(value).__name__)) if isinstance(value, str): if not value and not self.none_ok: @@ -182,12 +186,12 @@ class BaseType: """ raise NotImplementedError - def to_str(self, value): - """Get a string from the setting value. + # def to_str(self, value): + # """Get a string from the setting value. - The resulting string should be parseable again by from_str. - """ - raise NotImplementedError + # The resulting string should be parseable again by from_str. + # """ + # raise NotImplementedError # def to_py(self, value): # """Get a Python/YAML value from the setting value. @@ -241,10 +245,10 @@ class MappingType(BaseType): self._validate_valid_values(value.lower()) return self.MAPPING[value.lower()] - def to_str(self, value): - reverse_mapping = {v: k for k, v in self.MAPPING.items()} - assert len(self.MAPPING) == len(reverse_mapping) - return reverse_mapping[value] + # def to_str(self, value): + # reverse_mapping = {v: k for k, v in self.MAPPING.items()} + # assert len(self.MAPPING) == len(reverse_mapping) + # return reverse_mapping[value] class String(BaseType): @@ -299,6 +303,8 @@ class String(BaseType): self._basic_validation(value, pytype=str) if not value: return None + + self._validate_encoding(value) self._validate_valid_values(value) if self.forbidden is not None and any(c in value @@ -364,6 +370,10 @@ class List(BaseType): "be set!".format(self.length)) def from_str(self, value): + self._basic_validation(value, pytype=str) + if not value: + return None + try: json_val = json.loads(value) except ValueError as e: @@ -374,6 +384,7 @@ class List(BaseType): if not json_val: return None + self._validate_list(json_val) return [self.valtype.from_str(v) for v in json_val] def from_py(self, value): @@ -381,6 +392,7 @@ class List(BaseType): if not value: return None + self._validate_list(value) return [self.valtype.from_py(v) for v in value] @@ -396,8 +408,8 @@ class FlagList(List): _show_valtype = False - def __init__(self, none_ok=False, valid_values=None): - super().__init__(String(), none_ok) + def __init__(self, none_ok=False, valid_values=None, length=None): + super().__init__(valtype=String(), none_ok=none_ok, length=length) self.valtype.valid_values = valid_values def _check_duplicates(self, values): @@ -407,12 +419,16 @@ class FlagList(List): def from_str(self, value): vals = super().from_str(value) - self._check_duplicates(vals) + if vals is not None: + self._check_duplicates(vals) + self._validate_list(vals) return vals def from_py(self, value): vals = super().from_py(value) - self._check_duplicates(vals) + if vals is not None: + self._check_duplicates(vals) + self._validate_list(vals) return vals def complete(self): @@ -474,6 +490,13 @@ class BoolAsk(Bool): return 'ask' return super().from_py(value) + def from_str(self, value): + # basic validation unneeded if it's == 'ask' and done by Bool if we call + # super().from_str + if isinstance(value, str) and value.lower() == 'ask': + return 'ask' + return super().from_str(value) + class _Numeric(BaseType): @@ -521,11 +544,14 @@ class Int(_Numeric): """Base class for an integer setting.""" def from_str(self, value): + self._basic_validation(value, pytype=str) + if not value: + return None + try: intval = int(value) except ValueError: raise configexc.ValidationError(value, "must be an integer!") - # FIXME:conf should we check .is_integer() here?! return self.from_py(intval) def from_py(self, value): @@ -539,6 +565,10 @@ class Float(_Numeric): """Base class for a float setting.""" def from_str(self, value): + self._basic_validation(value, pytype=str) + if not value: + return None + try: floatval = float(value) except ValueError: @@ -546,7 +576,7 @@ class Float(_Numeric): return self.from_py(floatval) def from_py(self, value): - self._basic_validation(value, pytype=float) + self._basic_validation(value, pytype=(int, float)) self._validate_bounds(value) return value @@ -592,6 +622,7 @@ class PercOrInt(_Numeric): "({})!".format(self.minperc, self.maxperc)) def from_str(self, value): + self._basic_validation(value) if not value: return None @@ -599,13 +630,14 @@ class PercOrInt(_Numeric): return self.from_py(value) try: - floatval = float(value) + intval = int(value) except ValueError: raise configexc.ValidationError(value, "must be integer or percentage!") - return self.from_py(floatval) + return self.from_py(intval) def from_py(self, value): + """Expect a value like '42%' as string, or 23 as int.""" self._basic_validation(value, pytype=(int, str)) if not value: return @@ -615,14 +647,14 @@ class PercOrInt(_Numeric): raise configexc.ValidationError(value, "does not end with %") try: - floatval = float(value[:-1]) + intval = int(value[:-1]) except ValueError: raise configexc.ValidationError(value, "invalid percentage!") - if self.minperc is not None and floatval < self.minperc: + if self.minperc is not None and intval < self.minperc: raise configexc.ValidationError(value, "must be {}% or " "more!".format(self.minperc)) - if self.maxperc is not None and floatval > self.maxperc: + if self.maxperc is not None and intval > self.maxperc: raise configexc.ValidationError(value, "must be {}% or " "less!".format(self.maxperc)) @@ -823,11 +855,17 @@ class QtFont(Font): class Regex(BaseType): - """A regular expression.""" + """A regular expression. + + Attributes: + flags: The flags to be used when a regex is passed. + _regex_type: The Python type of a regex object. + """ def __init__(self, flags=0, none_ok=False): super().__init__(none_ok) self.flags = flags + self._regex_type = type(re.compile('')) def _compile_regex(self, pattern): """Check if the given regex is valid. @@ -859,8 +897,8 @@ class Regex(BaseType): return compiled def from_py(self, value): - regex_type = type(re.compile('')) - self._basic_validation(value, pytype=(str, regex_type)) + """Get a compiled regex from either a string or a regex object.""" + self._basic_validation(value, pytype=(str, self._regex_type)) if not value: return None elif isinstance(value, str): @@ -872,7 +910,7 @@ class Regex(BaseType): class Dict(BaseType): - """A JSON-like dictionary for custom HTTP headers.""" + """A dictionary of values.""" def __init__(self, keytype, valtype, *, fixed_keys=None, none_ok=False): super().__init__(none_ok) @@ -887,6 +925,10 @@ class Dict(BaseType): value, "Expected keys {}".format(self.fixed_keys)) def from_str(self, value): + self._basic_validation(value, pytype=str) + if not value: + return None + try: json_val = json.loads(value) except ValueError as e: @@ -981,7 +1023,7 @@ class FormatString(BaseType): return None try: - return value.format(**{k: '' for k in self.fields}) + value.format(**{k: '' for k in self.fields}) except (KeyError, IndexError) as e: raise configexc.ValidationError(value, "Invalid placeholder " "{}".format(e)) @@ -1004,6 +1046,9 @@ class ShellCommand(BaseType): self.placeholder = placeholder def from_str(self, value): + self._basic_validation(value, pytype=str) + if not value: + return None try: return self.from_py(shlex.split(value)) except ValueError as e: @@ -1217,6 +1262,8 @@ class SessionName(BaseType): def from_py(self, value): self._basic_validation(value, pytype=str) + if not value: + return None if value.startswith('_'): raise configexc.ValidationError(value, "may not start with '_'!") return value From 63bdee8b55764390a83d8c02ca2fc71ff2caa026 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 18:14:54 +0200 Subject: [PATCH 047/516] Initial configtype tests update --- tests/unit/config/test_configtypes.py | 1520 ++++++++++--------------- 1 file changed, 629 insertions(+), 891 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 378e78fdb..fc83697fa 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -19,6 +19,7 @@ """Tests for qutebrowser.config.configtypes.""" import re +import json import collections import itertools import os.path @@ -30,7 +31,8 @@ from PyQt5.QtGui import QColor, QFont from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.config import configtypes, configexc -from qutebrowser.utils import debug, utils +from qutebrowser.utils import debug, utils, qtutils +from qutebrowser.browser.network import pac class Font(QFont): @@ -99,8 +101,6 @@ def os_mock(mocker): class TestValidValues: - """Test ValidValues.""" - @pytest.fixture def klass(self): return configtypes.ValidValues @@ -167,50 +167,58 @@ class TestValidValues: obj2 = klass(*args2) assert (obj1 == obj2) == is_equal + def test_from_dict(self, klass): + """Test initializing from a list of dicts.""" + vv = klass({'foo': "foo desc"}, {'bar': "bar desc"}) + assert 'foo' in vv + assert 'bar' in vv + assert vv.descriptions['foo'] == "foo desc" + assert vv.descriptions['bar'] == "bar desc" + class TestBaseType: - """Test BaseType.""" - @pytest.fixture - def basetype(self): - return configtypes.BaseType() + def klass(self): + return configtypes.BaseType - @pytest.mark.parametrize('val, expected', [ - ('foobar', 'foobar'), - ('', None), - ]) - def test_transform(self, basetype, val, expected): - """Test transform with a value.""" - assert basetype.transform(val) == expected - - def test_validate_not_implemented(self, basetype): + def test_validate_valid_values_nop(self, klass): """Test validate without valid_values set.""" - with pytest.raises(NotImplementedError): - basetype.validate("foo") + klass()._validate_valid_values("foo") - def test_validate(self, basetype): + def test_validate_valid_values(self, klass): """Test validate with valid_values set.""" + basetype = klass() basetype.valid_values = configtypes.ValidValues('foo', 'bar') - basetype.validate('bar') + basetype._validate_valid_values('bar') with pytest.raises(configexc.ValidationError): - basetype.validate('baz') + basetype._validate_valid_values('baz') - @pytest.mark.parametrize('val', ['', 'foobar', 'snowman: ☃', 'foo bar']) - def test_basic_validation_valid(self, basetype, val): + @pytest.mark.parametrize('val', [None, '', 'foobar', 'snowman: ☃', + 'foo bar']) + def test_basic_validation_valid(self, klass, val): """Test _basic_validation with valid values.""" + basetype = klass() basetype.none_ok = True basetype._basic_validation(val) - @pytest.mark.parametrize('val', ['', '\x00']) - def test_basic_validation_invalid(self, basetype, val): + @pytest.mark.parametrize('val', [None, '', '\x00']) + def test_basic_validation_invalid(self, klass, val): """Test _basic_validation with invalid values.""" with pytest.raises(configexc.ValidationError): - basetype._basic_validation(val) + klass()._basic_validation(val) - def test_complete_none(self, basetype): + def test_basic_validation_pytype_valid(self, klass): + klass()._basic_validation([], pytype=list) + + def test_basic_validation_pytype_invalid(self, klass): + with pytest.raises(configexc.ValidationError, + match='expected a value of type str but got list'): + klass()._basic_validation([], pytype=str) + + def test_complete_none(self, klass): """Test complete with valid_values not set.""" - assert basetype.complete() is None + assert klass().complete() is None @pytest.mark.parametrize('valid_values, completions', [ # Without description @@ -223,15 +231,17 @@ class TestBaseType: ([('foo', "foo desc"), 'bar'], [('foo', "foo desc"), ('bar', "")]), ]) - def test_complete_without_desc(self, basetype, valid_values, completions): + def test_complete_without_desc(self, klass, valid_values, completions): """Test complete with valid_values set without description.""" + basetype = klass() basetype.valid_values = configtypes.ValidValues(*valid_values) assert basetype.complete() == completions - def test_get_name(self, basetype): - assert basetype.get_name() == 'BaseType' + def test_get_name(self, klass): + assert klass().get_name() == 'BaseType' - def test_get_valid_values(self, basetype): + def test_get_valid_values(self, klass): + basetype = klass() basetype.valid_values = configtypes.ValidValues('foo') assert basetype.get_valid_values() is basetype.valid_values @@ -252,9 +262,8 @@ class MappingSubclass(configtypes.MappingType): class TestMappingType: - """Test MappingType.""" - TESTS = { + None: None, '': None, 'one': 1, 'two': 2, @@ -265,18 +274,14 @@ class TestMappingType: def klass(self): return MappingSubclass - @pytest.mark.parametrize('val', sorted(TESTS.keys())) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + @pytest.mark.parametrize('val, expected', list(TESTS.items())) + def test_from_py(self, klass, val, expected): + assert klass(none_ok=True).from_py(val) == expected - @pytest.mark.parametrize('val', ['', 'one!', 'blah']) - def test_validate_invalid(self, klass, val): + @pytest.mark.parametrize('val', [None, 'one!', 'blah']) + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + klass().from_py(val) @pytest.mark.parametrize('typ', [configtypes.ColorSystem(), configtypes.Position(), @@ -287,8 +292,6 @@ class TestMappingType: class TestString: - """Test String.""" - @pytest.fixture(params=[configtypes.String, configtypes.UniqueCharString]) def klass(self, request): return request.param @@ -308,6 +311,7 @@ class TestString: @pytest.mark.parametrize('kwargs, val', [ ({'none_ok': True}, ''), # Empty with none_ok + ({'none_ok': True}, None), # None with none_ok ({}, "Test! :-)"), # Forbidden chars ({'forbidden': 'xyz'}, 'fobar'), @@ -319,8 +323,9 @@ class TestString: # valid_values ({'valid_values': configtypes.ValidValues('abcd')}, 'abcd'), ]) - def test_validate_valid(self, klass, kwargs, val): - klass(**kwargs).validate(val) + def test_from_py(self, klass, kwargs, val): + expected = None if not val else val + assert klass(**kwargs).from_py(val) == expected @pytest.mark.parametrize('kwargs, val', [ ({}, ''), # Empty without none_ok @@ -334,18 +339,17 @@ class TestString: ({'minlen': 2, 'maxlen': 3}, 'abcd'), # valid_values ({'valid_values': configtypes.ValidValues('blah')}, 'abcd'), + # Encoding + ({'encoding': 'ascii'}, 'fooäbar'), ]) - def test_validate_invalid(self, klass, kwargs, val): + def test_from_py_invalid(self, klass, kwargs, val): with pytest.raises(configexc.ValidationError): - klass(**kwargs).validate(val) + klass(**kwargs).from_py(val) - def test_validate_duplicate_invalid(self): + def test_from_py_duplicate_invalid(self): typ = configtypes.UniqueCharString() with pytest.raises(configexc.ValidationError): - typ.validate('foobar') - - def test_transform(self, klass): - assert klass().transform('fobar') == 'fobar' + typ.from_py('foobar') @pytest.mark.parametrize('value', [ None, @@ -373,59 +377,10 @@ class ListSubclass(configtypes.List): """ def __init__(self, none_ok_inner=False, none_ok_outer=False, length=None): - super().__init__(configtypes.BaseType(none_ok_inner), + super().__init__(configtypes.String(none_ok=none_ok_inner), none_ok=none_ok_outer, length=length) - self.inner_type.valid_values = configtypes.ValidValues('foo', - 'bar', 'baz') - - -class TestList: - - """Test List.""" - - @pytest.fixture - def klass(self): - return ListSubclass - - @pytest.mark.parametrize('val', ['', 'foo', 'foo,bar', 'foo, bar']) - def test_validate_valid(self, klass, val): - klass(none_ok_outer=True).validate(val) - - @pytest.mark.parametrize('val', ['', 'foo,,bar']) - def test_validate_invalid(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass().validate(val) - - def test_invalid_empty_value_none_ok(self, klass): - with pytest.raises(configexc.ValidationError): - klass(none_ok_outer=True).validate('foo,,bar') - with pytest.raises(configexc.ValidationError): - klass(none_ok_inner=True).validate('') - - @pytest.mark.parametrize('val', ['', 'foo,bar', 'foo, bar']) - def test_validate_length(self, klass, val): - klass(none_ok_outer=True, length=2).validate(val) - - @pytest.mark.parametrize('val', ['bar', 'foo,bar', 'foo,bar,foo,bar']) - def test_wrong_length(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass(length=3).validate(val) - - @pytest.mark.parametrize('val, expected', [ - ('foo', ['foo']), - ('foo,bar,baz', ['foo', 'bar', 'baz']), - ('', None), - ('foo, bar', ['foo', 'bar']) - ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - def test_get_name(self, klass): - assert klass().get_name() == 'ListSubclass of BaseType' - - def test_get_valid_values(self, klass): - expected = configtypes.ValidValues('foo', 'bar', 'baz') - assert klass().get_valid_values() == expected + self.valtype.valid_values = configtypes.ValidValues( + 'foo', 'bar', 'baz') class FlagListSubclass(configtypes.FlagList): @@ -437,17 +392,76 @@ class FlagListSubclass(configtypes.FlagList): combinable_values = ['foo', 'bar'] - def __init__(self, none_ok=False): - super().__init__(none_ok) - self.inner_type.valid_values = configtypes.ValidValues('foo', - 'bar', 'baz') - self.inner_type.none_ok = none_ok + def __init__(self, none_ok_inner=False, none_ok_outer=False, length=None): + # none_ok_inner is ignored, just here for compatibility with TestList + super().__init__(none_ok=none_ok_outer, length=length) + self.valtype.valid_values = configtypes.ValidValues( + 'foo', 'bar', 'baz') + + +class TestList: + + """Test List and FlagList.""" + + # FIXME:conf make sure from_str/from_py is called on valtype. + # FIXME:conf how to handle []? + + @pytest.fixture(params=[ListSubclass, FlagListSubclass]) + def klass(self, request): + return request.param + + @pytest.mark.parametrize('val', [['foo'], ['foo', 'bar']]) + def test_from_str(self, klass, val): + json_val = json.dumps(val) + assert klass().from_str(json_val) == val + + def test_from_str_empty(self, klass): + assert klass(none_ok_outer=True).from_str('') is None + + @pytest.mark.parametrize('val', ['', '[[', 'true']) + def test_from_str_invalid(self, klass, val): + with pytest.raises(configexc.ValidationError): + klass().from_str(val) + + @pytest.mark.parametrize('val', [['foo'], ['foo', 'bar']]) + def test_from_py(self, klass, val): + assert klass().from_py(val) == val + + @pytest.mark.parametrize('val', [[42], '["foo"]']) + def test_from_py_invalid(self, klass, val): + with pytest.raises(configexc.ValidationError): + klass().from_py(val) + + def test_invalid_empty_value_none_ok(self, klass): + with pytest.raises(configexc.ValidationError): + klass(none_ok_outer=True).from_py(['foo', '', 'bar']) + with pytest.raises(configexc.ValidationError): + klass(none_ok_inner=True).from_py(None) + + @pytest.mark.parametrize('val', [None, ['foo', 'bar']]) + def test_validate_length(self, klass, val): + klass(none_ok_outer=True, length=2).from_py(val) + + @pytest.mark.parametrize('val', [['a'], ['a', 'b'], ['a', 'b', 'c', 'd']]) + def test_wrong_length(self, klass, val): + with pytest.raises(configexc.ValidationError, + match='Exactly 3 values need to be set!'): + klass(length=3).from_py(val) + + def test_get_name(self, klass): + expected = { + ListSubclass: 'ListSubclass of String', + FlagListSubclass: 'FlagListSubclass', + } + assert klass().get_name() == expected[klass] + + def test_get_valid_values(self, klass): + expected = configtypes.ValidValues('foo', 'bar', 'baz') + assert klass().get_valid_values() == expected class TestFlagList: - """Test FlagList.""" - @pytest.fixture def klass(self): return FlagListSubclass @@ -457,31 +471,11 @@ class TestFlagList: """Return a FlagList with valid_values = None.""" return configtypes.FlagList - @pytest.mark.parametrize('val', ['', 'foo', 'foo,bar', 'foo,']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', ['qux', 'foo,qux', 'foo,foo']) - def test_validate_invalid(self, klass, val): + @pytest.mark.parametrize('val', [['qux'], ['foo', 'qux'], ['foo', 'foo']]) + def test_from_py_invalid(self, klass, val): + """Test invalid flag combinations (the rest is tested in TestList).""" with pytest.raises(configexc.ValidationError): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', ['', 'foo,', 'foo,,bar']) - def test_validate_empty_value_not_okay(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass(none_ok=False).validate(val) - - @pytest.mark.parametrize('val, expected', [ - ('', None), - ('foo', ['foo']), - ('foo,bar', ['foo', 'bar']), - ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - @pytest.mark.parametrize('val', ['spam', 'spam,eggs']) - def test_validate_values_none(self, klass_valid_none, val): - klass_valid_none().validate(val) + klass(none_ok_outer=True).from_py(val) def test_complete(self, klass): """Test completing by doing some samples.""" @@ -507,15 +501,9 @@ class TestFlagList: def test_complete_no_valid_values(self, klass_valid_none): assert klass_valid_none().complete() is None - def test_get_name(self, klass): - """Make sure the name has no "of ..." in it.""" - assert klass().get_name() == 'FlagListSubclass' - class TestBool: - """Test Bool.""" - TESTS = { '1': True, 'yes': True, @@ -541,23 +529,26 @@ class TestBool: return configtypes.Bool @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - @pytest.mark.parametrize('val', sorted(TESTS)) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + def test_from_str_valid(self, klass, val, expected): + assert klass(none_ok=True).from_str(val) == expected @pytest.mark.parametrize('val', INVALID) - def test_validate_invalid(self, klass, val): + def test_from_str_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) + klass().from_str(val) + + @pytest.mark.parametrize('val', [True, False, None]) + def test_from_py_valid(self, klass, val): + assert klass(none_ok=True).from_py(val) is val + + @pytest.mark.parametrize('val', [None, 42]) + def test_from_py_invalid(self, klass, val): + with pytest.raises(configexc.ValidationError): + klass().from_py(val) class TestBoolAsk: - """Test BoolAsk.""" - TESTS = { 'ask': 'ask', 'ASK': 'ask', @@ -571,195 +562,245 @@ class TestBoolAsk: return configtypes.BoolAsk @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - @pytest.mark.parametrize('val', sorted(TESTS)) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + def test_from_str_valid(self, klass, val, expected): + assert klass(none_ok=True).from_str(val) == expected @pytest.mark.parametrize('val', INVALID) - def test_validate_invalid(self, klass, val): + def test_from_str_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) + klass().from_str(val) + + @pytest.mark.parametrize('val', [True, False, None, 'ask']) + def test_from_py_valid(self, klass, val): + assert klass(none_ok=True).from_py(val) == val + + @pytest.mark.parametrize('val', [None, 42]) + def test_from_py_invalid(self, klass, val): + with pytest.raises(configexc.ValidationError): + klass().from_py(val) + + +class TestNumeric: + + """Test the bounds handling in _Numeric.""" + + @pytest.fixture + def klass(self): + return configtypes._Numeric + + def test_minval_gt_maxval(self, klass): + with pytest.raises(ValueError): + klass(minval=2, maxval=1) + + def test_special_bounds(self, klass): + """Test passing strings as bounds.""" + numeric = klass(minval='maxint', maxval='maxint64') + assert numeric.minval == qtutils.MAXVALS['int'] + assert numeric.maxval == qtutils.MAXVALS['int64'] + + @pytest.mark.parametrize('kwargs, val, valid', [ + ({}, 1337, True), + ({}, 0, True), + ({'minval': 2}, 2, True), + ({'maxval': 2}, 2, True), + ({'minval': 2, 'maxval': 3}, 2, True), + ({'minval': 2, 'maxval': 3}, 3, True), + ({}, None, True), + + ({'minval': 2}, 1, False), + ({'maxval': 2}, 3, False), + ({'minval': 2, 'maxval': 3}, 1, False), + ({'minval': 2, 'maxval': 3}, 4, False), + ]) + def test_validate_invalid(self, klass, kwargs, val, valid): + if valid: + klass(**kwargs)._validate_bounds(val) + else: + with pytest.raises(configexc.ValidationError): + klass(**kwargs)._validate_bounds(val) + + def test_suffix(self, klass): + """Test suffix in validation message.""" + with pytest.raises(configexc.ValidationError, + match='must be 2% or smaller'): + klass(maxval=2)._validate_bounds(3, suffix='%') class TestInt: - """Test Int.""" - @pytest.fixture def klass(self): return configtypes.Int - def test_minval_gt_maxval(self, klass): - with pytest.raises(ValueError): - klass(minval=2, maxval=1) - - @pytest.mark.parametrize('kwargs, val', [ - ({}, '1337'), - ({}, '0'), - ({'none_ok': True}, ''), - ({'minval': 2}, '2'), - ({'maxval': 2}, '2'), - ({'minval': 2, 'maxval': 3}, '2'), - ({'minval': 2, 'maxval': 3}, '3'), + @pytest.mark.parametrize('kwargs, val, expected', [ + ({}, '1337', 1337), + ({}, '0', 0), + ({'none_ok': True}, '', None), + ({'minval': 2}, '2', 2), ]) - def test_validate_valid(self, klass, kwargs, val): - klass(**kwargs).validate(val) + def test_from_str_valid(self, klass, kwargs, val, expected): + assert klass(**kwargs).from_str(val) == expected @pytest.mark.parametrize('kwargs, val', [ ({}, ''), ({}, '2.5'), ({}, 'foobar'), - ({'minval': 2}, '1'), - ({'maxval': 2}, '3'), ({'minval': 2, 'maxval': 3}, '1'), - ({'minval': 2, 'maxval': 3}, '4'), ]) - def test_validate_invalid(self, klass, kwargs, val): + def test_from_str_invalid(self, klass, kwargs, val): with pytest.raises(configexc.ValidationError): - klass(**kwargs).validate(val) + klass(**kwargs).from_str(val) - @pytest.mark.parametrize('val, expected', [('1', 1), ('1337', 1337), - ('', None)]) - def test_transform(self, klass, val, expected): - assert klass(none_ok=True).transform(val) == expected + @pytest.mark.parametrize('kwargs, val', [ + ({}, 1337), + ({}, 0), + ({'none_ok': True}, None), + ({'minval': 2}, 2), + ]) + def test_from_py_valid(self, klass, kwargs, val): + assert klass(**kwargs).from_py(val) == val + + @pytest.mark.parametrize('kwargs, val', [ + ({}, ''), + ({}, 2.5), + ({}, 'foobar'), + ({'minval': 2, 'maxval': 3}, 1), + ]) + def test_from_py_invalid(self, klass, kwargs, val): + with pytest.raises(configexc.ValidationError): + klass(**kwargs).from_py(val) class TestFloat: - """Test Float.""" - @pytest.fixture def klass(self): return configtypes.Float - def test_minval_gt_maxval(self, klass): - with pytest.raises(ValueError): - klass(minval=2, maxval=1) - - @pytest.mark.parametrize('kwargs, val', [ - ({}, '1337.42'), - ({}, '0'), - ({}, '1337'), - ({'none_ok': True}, ''), - ({'minval': 2}, '2.00'), - ({'maxval': 2}, '2.00'), - ({'minval': 2, 'maxval': 3}, '2.00'), - ({'minval': 2, 'maxval': 3}, '3.00'), + @pytest.mark.parametrize('kwargs, val, expected', [ + ({}, '1337', 1337), + ({}, '1337.42', 1337.42), + ({'none_ok': True}, '', None), + ({'minval': 2.00}, '2.00', 2.00), ]) - def test_validate_valid(self, klass, kwargs, val): - klass(**kwargs).validate(val) + def test_from_str_valid(self, klass, kwargs, val, expected): + assert klass(**kwargs).from_str(val) == expected @pytest.mark.parametrize('kwargs, val', [ ({}, ''), - ({}, '2.5.2'), ({}, 'foobar'), - ({'minval': 2}, '1.99'), - ({'maxval': 2}, '2.01'), - ({'minval': 2, 'maxval': 3}, '1.99'), ({'minval': 2, 'maxval': 3}, '3.01'), ]) - def test_validate_invalid(self, klass, kwargs, val): + def test_from_str_invalid(self, klass, kwargs, val): with pytest.raises(configexc.ValidationError): - klass(**kwargs).validate(val) + klass(**kwargs).from_str(val) - @pytest.mark.parametrize('val, expected', [ - ('1337', 1337.00), - ('1337.42', 1337.42), - ('', None), + @pytest.mark.parametrize('kwargs, val', [ + ({}, 1337), + ({}, 0), + ({}, 1337.42), + ({'none_ok': True}, None), + ({'minval': 2}, 2.01), ]) - def test_transform(self, klass, val, expected): - assert klass(none_ok=True).transform(val) == expected + def test_from_py_valid(self, klass, kwargs, val): + assert klass(**kwargs).from_py(val) == val + + @pytest.mark.parametrize('kwargs, val', [ + ({}, ''), + ({}, 'foobar'), + ({'minval': 2, 'maxval': 3}, 1.99), + ]) + def test_from_py_invalid(self, klass, kwargs, val): + with pytest.raises(configexc.ValidationError): + klass(**kwargs).from_py(val) class TestPerc: - """Test Perc.""" - @pytest.fixture def klass(self): return configtypes.Perc - def test_minval_gt_maxval(self, klass): - with pytest.raises(ValueError): - klass(minval=2, maxval=1) - - @pytest.mark.parametrize('kwargs, val', [ - ({}, '1337%'), - ({'minval': 2}, '2%'), - ({'maxval': 2}, '2%'), - ({'minval': 2, 'maxval': 3}, '2%'), - ({'minval': 2, 'maxval': 3}, '3%'), - ({'none_ok': True}, ''), + @pytest.mark.parametrize('kwargs, val, expected', [ + ({}, '1337%', 1337), + ({}, '1337.42%', 1337.42), + ({'none_ok': True}, '', None), + ({'maxval': 2}, '2%', 2), ]) - def test_validate_valid(self, klass, kwargs, val): - klass(**kwargs).validate(val) + def test_from_str_valid(self, klass, kwargs, val, expected): + assert klass(**kwargs).from_str(val) == expected @pytest.mark.parametrize('kwargs, val', [ ({}, '1337'), ({}, '1337%%'), ({}, 'foobar'), + ({}, 'foobar%'), ({}, ''), ({'minval': 2}, '1%'), ({'maxval': 2}, '3%'), ({'minval': 2, 'maxval': 3}, '1%'), ({'minval': 2, 'maxval': 3}, '4%'), ]) - def test_validate_invalid(self, klass, kwargs, val): + def test_from_str_invalid(self, klass, kwargs, val): with pytest.raises(configexc.ValidationError): - klass(**kwargs).validate(val) + klass(**kwargs).from_str(val) - @pytest.mark.parametrize('val, expected', [ - ('', None), - ('1337%', 1337), + @pytest.mark.parametrize('kwargs, val, expected', [ + ({}, '1337.42%', 1337.42), + ({'none_ok': True}, None, None), + ({'minval': 2}, '2.01%', 2.01), ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + def test_from_py_valid(self, klass, kwargs, val, expected): + assert klass(**kwargs).from_py(val) == expected + + @pytest.mark.parametrize('kwargs, val', [ + ({}, ''), + ({}, 'foobar'), + ({}, 23), + ({'minval': 2, 'maxval': 3}, '1.99%'), + ]) + def test_from_py_invalid(self, klass, kwargs, val): + with pytest.raises(configexc.ValidationError): + klass(**kwargs).from_py(val) class TestPercOrInt: - """Test PercOrInt.""" - @pytest.fixture def klass(self): return configtypes.PercOrInt - def test_minint_gt_maxint(self, klass): - with pytest.raises(ValueError): - klass(minint=2, maxint=1) - def test_minperc_gt_maxperc(self, klass): with pytest.raises(ValueError): klass(minperc=2, maxperc=1) - @pytest.mark.parametrize('kwargs, val', [ - ({}, '1337%'), - ({}, '1337'), - ({'none_ok': True}, ''), + def test_special_bounds(self, klass): + """Test passing strings as bounds.""" + poi = klass(minperc='maxint', maxperc='maxint64') + assert poi.minperc == qtutils.MAXVALS['int'] + assert poi.maxperc == qtutils.MAXVALS['int64'] - ({'minperc': 2}, '2%'), - ({'maxperc': 2}, '2%'), - ({'minperc': 2, 'maxperc': 3}, '2%'), - ({'minperc': 2, 'maxperc': 3}, '3%'), + @pytest.mark.parametrize('kwargs, val, expected', [ + ({}, '1337%', '1337%'), + ({}, '1337', 1337), + ({'none_ok': True}, '', None), - ({'minint': 2}, '2'), - ({'maxint': 2}, '2'), - ({'minint': 2, 'maxint': 3}, '2'), - ({'minint': 2, 'maxint': 3}, '3'), + ({'minperc': 2}, '2%', '2%'), + ({'maxperc': 2}, '2%', '2%'), + ({'minperc': 2, 'maxperc': 3}, '2%', '2%'), + ({'minperc': 2, 'maxperc': 3}, '3%', '3%'), - ({'minperc': 2, 'maxperc': 3}, '1'), - ({'minperc': 2, 'maxperc': 3}, '4'), - ({'minint': 2, 'maxint': 3}, '1%'), - ({'minint': 2, 'maxint': 3}, '4%'), + ({'minperc': 2, 'maxperc': 3}, '1', 1), + ({'minperc': 2, 'maxperc': 3}, '4', 4), + ({'minint': 2, 'maxint': 3}, '1%', '1%'), + ({'minint': 2, 'maxint': 3}, '4%', '4%'), ]) - def test_validate_valid(self, klass, kwargs, val): - klass(**kwargs).validate(val) + def test_from_str_valid(self, klass, kwargs, val, expected): + klass(**kwargs).from_str(val) == expected @pytest.mark.parametrize('kwargs, val', [ ({}, '1337%%'), + ({}, '1337.42%'), ({}, 'foobar'), ({}, ''), @@ -773,23 +814,22 @@ class TestPercOrInt: ({'minint': 2, 'maxint': 3}, '1'), ({'minint': 2, 'maxint': 3}, '4'), ]) - def test_validate_invalid(self, klass, kwargs, val): + def test_from_str_invalid(self, klass, kwargs, val): with pytest.raises(configexc.ValidationError): - klass(**kwargs).validate(val) + klass(**kwargs).from_str(val) - @pytest.mark.parametrize('val, expected', [ - ('', None), - ('1337%', '1337%'), - ('1337', '1337'), - ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + @pytest.mark.parametrize('val', ['1337%', 1337, None]) + def test_from_py_valid(self, klass, val): + klass(none_ok=True).from_py(val) == val + + @pytest.mark.parametrize('val', ['1337%%', '1337']) + def test_from_py_invalid(self, klass, val): + with pytest.raises(configexc.ValidationError): + klass().from_py(val) class TestCommand: - """Test Command.""" - @pytest.fixture(autouse=True) def patch(self, monkeypatch, stubs): """Patch the cmdutils module to provide fake commands.""" @@ -804,18 +844,14 @@ class TestCommand: @pytest.mark.parametrize('val', ['', 'cmd1', 'cmd2', 'cmd1 foo bar', 'cmd2 baz fish']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + def test_from_py_valid(self, klass, val): + expected = None if not val else val + klass(none_ok=True).from_py(val) == expected @pytest.mark.parametrize('val', ['', 'cmd3', 'cmd3 foo bar', ' ']) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', [('foo bar', 'foo bar'), - ('', None)]) - def test_transform(self, val, expected, klass): - assert klass().transform(val) == expected + klass().from_py(val) def test_complete(self, klass): """Test completion.""" @@ -825,46 +861,11 @@ class TestCommand: assert ('cmd2', "desc 2") in items -class TestColorSystem: - - """Test ColorSystem.""" - - TESTS = { - 'RGB': QColor.Rgb, - 'rgb': QColor.Rgb, - 'HSV': QColor.Hsv, - 'hsv': QColor.Hsv, - 'HSL': QColor.Hsl, - 'hsl': QColor.Hsl, - 'none': None, - 'None': None, - '': None, - } - INVALID = ['RRGB', 'HSV ', ''] # '' is invalid with none_ok=False - - @pytest.fixture - def klass(self): - return configtypes.ColorSystem - - @pytest.mark.parametrize('val', sorted(TESTS)) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', INVALID) - def test_validate_invalid(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - class ColorTests: """Generator for tests for TestColors.""" - TYPES = [configtypes.QtColor, configtypes.CssColor, configtypes.QssColor] + TYPES = [configtypes.QtColor, configtypes.QssColor] TESTS = [ ('#123', TYPES), @@ -872,7 +873,7 @@ class ColorTests: ('#111222333', TYPES), ('#111122223333', TYPES), ('red', TYPES), - ('', TYPES), + (None, TYPES), ('#00000G', []), ('#123456789ABCD', []), @@ -884,7 +885,6 @@ class ColorTests: ('rgb(0, 0, 0)', [configtypes.QssColor]), ('rgb(0,0,0)', [configtypes.QssColor]), - ('-foobar(42)', [configtypes.CssColor]), ('rgba(255, 255, 255, 1.0)', [configtypes.QssColor]), ('hsv(10%,10%,10%)', [configtypes.QssColor]), @@ -916,7 +916,7 @@ class ColorTests: class TestColors: - """Test QtColor/CssColor/QssColor.""" + """Test QtColor/QssColor.""" TESTS = ColorTests() @@ -935,28 +935,23 @@ class TestColors: assert self.TESTS.invalid @pytest.mark.parametrize('klass, val', TESTS.valid) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('klass, val', TESTS.invalid) - def test_validate_invalid(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass().validate(val) - - def test_validate_invalid_empty(self, klass_fixt): - with pytest.raises(configexc.ValidationError): - klass_fixt().validate('') - - @pytest.mark.parametrize('klass, val', TESTS.valid) - def test_transform(self, klass, val): - """Test transform of all color types.""" + def test_from_py_valid(self, klass, val): if not val: expected = None elif klass is configtypes.QtColor: expected = QColor(val) else: expected = val - assert klass().transform(val) == expected + assert klass(none_ok=True).from_py(val) == expected + + @pytest.mark.parametrize('klass, val', TESTS.invalid) + def test_from_py_invalid(self, klass, val): + with pytest.raises(configexc.ValidationError): + klass().from_py(val) + + def test_validate_invalid_empty(self, klass_fixt): + with pytest.raises(configexc.ValidationError): + klass_fixt().from_py('') FontDesc = collections.namedtuple('FontDesc', @@ -1026,9 +1021,28 @@ class TestFont: def qtfont_class(self): return configtypes.QtFont - @pytest.mark.parametrize('val', sorted(list(TESTS)) + ['']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + @pytest.mark.parametrize('val, desc', sorted(TESTS.items()) + [(None, None)]) + def test_from_py_valid(self, klass, val, desc): + if desc is None: + expected = None + elif klass is configtypes.Font: + expected = val + elif klass is configtypes.QtFont: + expected = Font.fromdesc(desc) + assert klass(none_ok=True).from_py(val) == expected + + def test_qtfont_float(self, qtfont_class): + """Test QtFont's transform with a float as point size. + + We can't test the point size for equality as Qt seems to do some + rounding as appropriate. + """ + value = Font(qtfont_class().from_py('10.5pt "Foobar Neue"')) + assert value.family() == 'Foobar Neue' + assert value.weight() == QFont.Normal + assert value.style() == QFont.StyleNormal + assert value.pointSize() >= 10 + assert value.pointSize() <= 11 @pytest.mark.parametrize('val', [ pytest.param('green "Foobar Neue"', marks=font_xfail), @@ -1042,49 +1056,16 @@ class TestFont: pytest.param('10pt', marks=font_xfail), pytest.param('10pt ""', marks=font_xfail), '', + None, ]) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - def test_validate_invalid_none(self, klass): - """Test validate with empty value and none_ok=False. - - Not contained in test_validate_invalid so it can be marked xfail. - """ - with pytest.raises(configexc.ValidationError): - klass().validate('') - - @pytest.mark.parametrize('string', sorted(TESTS)) - def test_transform_font(self, font_class, string): - assert font_class().transform(string) == string - - @pytest.mark.parametrize('string, desc', sorted(TESTS.items())) - def test_transform_qtfont(self, qtfont_class, string, desc): - assert Font(qtfont_class().transform(string)) == Font.fromdesc(desc) - - def test_transform_qtfont_float(self, qtfont_class): - """Test QtFont's transform with a float as point size. - - We can't test the point size for equality as Qt seems to do some - rounding as appropriate. - """ - value = Font(qtfont_class().transform('10.5pt "Foobar Neue"')) - assert value.family() == 'Foobar Neue' - assert value.weight() == QFont.Normal - assert value.style() == QFont.StyleNormal - assert value.pointSize() >= 10 - assert value.pointSize() <= 11 - - def test_transform_empty(self, klass): - assert klass().transform('') is None + klass().from_py(val) class TestFontFamily: - """Test FontFamily.""" - - TESTS = ['"Foobar Neue"', 'inconsolatazi4', 'Foobar', ''] + TESTS = ['"Foobar Neue"', 'inconsolatazi4', 'Foobar', None] INVALID = [ '10pt "Foobar Neue"', '10PT "Foobar Neue"', @@ -1100,7 +1081,7 @@ class TestFontFamily: 'oblique 10pt "Foobar Neue"', 'normal bold 10pt "Foobar Neue"', 'bold italic 10pt "Foobar Neue"', - '', # with none_ok=False + None, # with none_ok=False ] @pytest.fixture @@ -1108,46 +1089,44 @@ class TestFontFamily: return configtypes.FontFamily @pytest.mark.parametrize('val', TESTS) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + def test_from_py_valid(self, klass, val): + klass(none_ok=True).from_py(val) == val @pytest.mark.parametrize('val', INVALID) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', [('foobar', 'foobar'), - ('', None)]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + klass().from_py(val) class TestRegex: - """Test Regex.""" - @pytest.fixture def klass(self): return configtypes.Regex - @pytest.mark.parametrize('val', [r'(foo|bar)?baz[fis]h', '']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + @pytest.mark.parametrize('val', [ + r'(foo|bar)?baz[fis]h', + re.compile('foobar'), + None + ]) + def test_from_py_valid(self, klass, val): + expected = None if val is None else RegexEq(val) + assert klass(none_ok=True).from_py(val) == expected @pytest.mark.parametrize('val', [ pytest.param(r'(foo|bar))?baz[fis]h', id='unmatched parens'), pytest.param('', id='empty'), pytest.param('(' * 500, id='too many parens'), ]) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) + klass().from_py(val) @pytest.mark.parametrize('val', [ r'foo\Xbar', r'foo\Cbar', ]) - def test_validate_maybe_valid(self, klass, val): + def test_from_py_maybe_valid(self, klass, val): """Those values are valid on some Python versions (and systems?). On others, they raise a DeprecationWarning because of an invalid @@ -1155,17 +1134,10 @@ class TestRegex: ValidationError. """ try: - klass().validate(val) + klass().from_py(val) except configexc.ValidationError: pass - @pytest.mark.parametrize('val, expected', [ - (r'foobar', RegexEq(r'foobar')), - ('', None), - ]) - def test_transform_empty(self, klass, val, expected): - assert klass().transform(val) == expected - @pytest.mark.parametrize('warning', [ Warning('foo'), DeprecationWarning('foo'), ]) @@ -1174,11 +1146,12 @@ class TestRegex: The warning should be passed. """ + regex = klass() m = mocker.patch('qutebrowser.config.configtypes.re') m.compile.side_effect = lambda *args: warnings.warn(warning) m.error = re.error with pytest.raises(type(warning)): - klass().validate('foo') + regex.from_py('foo') def test_bad_pattern_warning(self, mocker, klass): """Test a simulated bad pattern warning. @@ -1186,12 +1159,63 @@ class TestRegex: This only seems to happen with Python 3.5, so we simulate this for better coverage. """ + regex = klass() m = mocker.patch('qutebrowser.config.configtypes.re') m.compile.side_effect = lambda *args: warnings.warn(r'bad escape \C', DeprecationWarning) m.error = re.error with pytest.raises(configexc.ValidationError): - klass().validate('foo') + regex.from_py('foo') + + +class TestDict: + + # FIXME:conf make sure from_str/from_py is called on keytype/valtype. + # FIXME:conf how to handle {}? + + @pytest.fixture + def klass(self): + return configtypes.Dict + + @pytest.mark.parametrize('val', [ + {"foo": "bar"}, + {"foo": "bar", "baz": "fish"}, + '', # empty value with none_ok=true + {}, # ditto + ]) + def test_from_str_valid(self, klass, val): + expected = None if not val else val + if isinstance(val, dict): + val = json.dumps(val) + d = klass(keytype=configtypes.String(), valtype=configtypes.String(), + none_ok=True) + assert d.from_str(val) == expected + + @pytest.mark.parametrize('val', [ + '["foo"]', # valid json but not a dict + '{"hello": 23}', # non-string as value + '', # empty value with none_ok=False + '[invalid', # invalid json + ]) + def test_from_str_invalid(self, klass, val): + d = klass(keytype=configtypes.String(), valtype=configtypes.String()) + with pytest.raises(configexc.ValidationError): + d.from_str(val) + + @pytest.mark.parametrize('val', [ + {"one": "1"}, # missing key + {"one": "1", "two": "2", "three": "3"}, # extra key + ]) + @pytest.mark.parametrize('from_str', [True, False]) + def test_fixed_keys(self, klass, val, from_str): + d = klass(keytype=configtypes.String(), valtype=configtypes.String(), + fixed_keys=['one', 'two']) + + with pytest.raises(configexc.ValidationError): + if from_str: + d.from_str(json.dumps(val)) + else: + d.from_py(val) def unrequired_class(**kwargs): @@ -1210,93 +1234,23 @@ class TestFile: def file_class(self): return configtypes.File - def _expected(self, klass, arg): - """Get the expected value.""" - if not arg: - return None - elif klass is configtypes.File: - return arg - elif klass is unrequired_class: - return arg - else: - assert False, klass - - def test_validate_empty(self, klass): + def test_from_py_empty(self, klass): with pytest.raises(configexc.ValidationError): - klass().validate('') + klass().from_py('') - def test_validate_empty_none_ok(self, klass): - klass(none_ok=True).validate('') + def test_from_py_empty_none_ok(self, klass): + assert klass(none_ok=True).from_py('') is None - def test_validate_does_not_exist_file(self, os_mock): - """Test validate with a file which does not exist (File).""" + def test_from_py_does_not_exist_file(self, os_mock): + """Test from_py with a file which does not exist (File).""" os_mock.path.isfile.return_value = False with pytest.raises(configexc.ValidationError): - configtypes.File().validate('foobar') + configtypes.File().from_py('foobar') - def test_validate_does_not_exist_optional_file(self, os_mock): - """Test validate with a file which does not exist (File).""" + def test_from_py_does_not_exist_optional_file(self, os_mock): + """Test from_py with a file which does not exist (File).""" os_mock.path.isfile.return_value = False - configtypes.File(required=False).validate('foobar') - - def test_validate_exists_abs(self, klass, os_mock): - """Test validate with a file which does exist.""" - os_mock.path.isfile.return_value = True - os_mock.path.isabs.return_value = True - klass().validate('foobar') - - def test_validate_exists_rel(self, klass, os_mock, monkeypatch): - """Test validate with a relative path to an existing file.""" - monkeypatch.setattr( - 'qutebrowser.config.configtypes.standarddir.config', - lambda: '/home/foo/.config/') - os_mock.path.isfile.return_value = True - os_mock.path.isabs.return_value = False - klass().validate('foobar') - os_mock.path.join.assert_called_once_with( - '/home/foo/.config/', 'foobar') - - @pytest.mark.parametrize('configtype, value, raises', [ - pytest.param(configtypes.File(), 'foobar', True, id='file-foobar'), - pytest.param(configtypes.File(required=False), 'foobar', False, - id='file-optional-foobar'), - ]) - def test_validate_rel_inexistent(self, os_mock, monkeypatch, configtype, - value, raises): - """Test with a relative path and standarddir.config returning None.""" - monkeypatch.setattr( - 'qutebrowser.config.configtypes.standarddir.config', - lambda: 'this/does/not/exist') - os_mock.path.isabs.return_value = False - os_mock.path.isfile.side_effect = os.path.isfile - - if raises: - with pytest.raises(configexc.ValidationError): - configtype.validate(value) - else: - configtype.validate(value) - - def test_validate_expanduser(self, klass, os_mock): - """Test if validate expands the user correctly.""" - os_mock.path.isfile.side_effect = (lambda path: - path == '/home/foo/foobar') - os_mock.path.isabs.return_value = True - klass().validate('~/foobar') - - def test_validate_expandvars(self, klass, os_mock): - """Test if validate expands the environment vars correctly.""" - os_mock.path.isfile.side_effect = (lambda path: - path == '/home/foo/foobar') - os_mock.path.isabs.return_value = True - klass().validate('$HOME/foobar') - - def test_validate_invalid_encoding(self, klass, os_mock, - unicode_encode_err): - """Test validate with an invalid encoding, e.g. LC_ALL=C.""" - os_mock.path.isfile.side_effect = unicode_encode_err - os_mock.path.isabs.side_effect = unicode_encode_err - with pytest.raises(configexc.ValidationError): - klass().validate('foobar') + assert unrequired_class().from_py('foobar') == 'foobar' @pytest.mark.parametrize('val, expected', [ ('/foobar', '/foobar'), @@ -1304,106 +1258,148 @@ class TestFile: ('$HOME/foobar', '/home/foo/foobar'), ('', None), ]) - def test_transform_abs(self, klass, os_mock, val, expected): - assert klass().transform(val) == self._expected(klass, expected) + def test_from_py_exists_abs(self, klass, os_mock, val, expected): + """Test from_py with a file which does exist.""" + os_mock.path.isfile.return_value = True + assert klass(none_ok=True).from_py(val) == expected - def test_transform_relative(self, klass, os_mock, monkeypatch): - """Test transform() with relative dir and an available configdir.""" - os_mock.path.isabs.return_value = False + def test_from_py_exists_rel(self, klass, os_mock, monkeypatch): + """Test from_py with a relative path to an existing file.""" monkeypatch.setattr( 'qutebrowser.config.configtypes.standarddir.config', - lambda: '/configdir') - expected = self._expected(klass, '/configdir/foo') - assert klass().transform('foo') == expected + lambda: '/home/foo/.config') + os_mock.path.isfile.return_value = True + os_mock.path.isabs.return_value = False + assert klass().from_py('foobar') == '/home/foo/.config/foobar' + os_mock.path.join.assert_called_once_with( + '/home/foo/.config', 'foobar') + + def test_from_py_expanduser(self, klass, os_mock): + """Test if from_py expands the user correctly.""" + os_mock.path.isfile.side_effect = (lambda path: + path == '/home/foo/foobar') + os_mock.path.isabs.return_value = True + assert klass().from_py('~/foobar') == '/home/foo/foobar' + + def test_from_py_expandvars(self, klass, os_mock): + """Test if from_py expands the environment vars correctly.""" + os_mock.path.isfile.side_effect = (lambda path: + path == '/home/foo/foobar') + os_mock.path.isabs.return_value = True + assert klass().from_py('$HOME/foobar') == '/home/foo/foobar' + + def test_from_py_invalid_encoding(self, klass, os_mock, + unicode_encode_err): + """Test from_py with an invalid encoding, e.g. LC_ALL=C.""" + os_mock.path.isfile.side_effect = unicode_encode_err + os_mock.path.isabs.side_effect = unicode_encode_err + with pytest.raises(configexc.ValidationError): + klass().from_py('foobar') class TestDirectory: - """Test Directory.""" - @pytest.fixture def klass(self): return configtypes.Directory - def test_validate_empty(self, klass): - """Test validate with empty string and none_ok = False.""" + def test_from_py_empty(self, klass): + """Test from_py with empty string and none_ok = False.""" with pytest.raises(configexc.ValidationError): - klass().validate('') + klass().from_py('') - def test_validate_empty_none_ok(self, klass): - """Test validate with empty string and none_ok = True.""" + def test_from_py_empty_none_ok(self, klass): + """Test from_py with empty string and none_ok = True.""" t = configtypes.Directory(none_ok=True) - t.validate('') + assert t.from_py(None) is None - def test_validate_does_not_exist(self, klass, os_mock): - """Test validate with a directory which does not exist.""" + def test_from_py_does_not_exist(self, klass, os_mock): + """Test from_py with a directory which does not exist.""" os_mock.path.isdir.return_value = False with pytest.raises(configexc.ValidationError): - klass().validate('foobar') + klass().from_py('foobar') - def test_validate_exists_abs(self, klass, os_mock): - """Test validate with a directory which does exist.""" + def test_from_py_exists_abs(self, klass, os_mock): + """Test from_py with a directory which does exist.""" os_mock.path.isdir.return_value = True os_mock.path.isabs.return_value = True - klass().validate('foobar') + assert klass().from_py('foobar') == 'foobar' - def test_validate_exists_not_abs(self, klass, os_mock): - """Test validate with a dir which does exist but is not absolute.""" + def test_from_py_exists_not_abs(self, klass, os_mock): + """Test from_py with a dir which does exist but is not absolute.""" os_mock.path.isdir.return_value = True os_mock.path.isabs.return_value = False with pytest.raises(configexc.ValidationError): - klass().validate('foobar') + klass().from_py('foobar') - def test_validate_expanduser(self, klass, os_mock): - """Test if validate expands the user correctly.""" + def test_from_py_expanduser(self, klass, os_mock): + """Test if from_py expands the user correctly.""" os_mock.path.isdir.side_effect = (lambda path: path == '/home/foo/foobar') os_mock.path.isabs.return_value = True - klass().validate('~/foobar') + assert klass().from_py('~/foobar') == '/home/foo/foobar' os_mock.path.expanduser.assert_called_once_with('~/foobar') - def test_validate_expandvars(self, klass, os_mock, monkeypatch): - """Test if validate expands the user correctly.""" + def test_from_py_expandvars(self, klass, os_mock, monkeypatch): + """Test if from_py expands the user correctly.""" os_mock.path.isdir.side_effect = (lambda path: path == '/home/foo/foobar') os_mock.path.isabs.return_value = True - klass().validate('$HOME/foobar') + assert klass().from_py('$HOME/foobar') == '/home/foo/foobar' os_mock.path.expandvars.assert_called_once_with('$HOME/foobar') - def test_validate_invalid_encoding(self, klass, os_mock, - unicode_encode_err): - """Test validate with an invalid encoding, e.g. LC_ALL=C.""" + def test_from_py_invalid_encoding(self, klass, os_mock, + unicode_encode_err): + """Test from_py with an invalid encoding, e.g. LC_ALL=C.""" os_mock.path.isdir.side_effect = unicode_encode_err os_mock.path.isabs.side_effect = unicode_encode_err with pytest.raises(configexc.ValidationError): - klass().validate('foobar') + klass().from_py('foobar') - def test_transform(self, klass, os_mock): - assert klass().transform('~/foobar') == '/home/foo/foobar' - os_mock.path.expanduser.assert_called_once_with('~/foobar') - def test_transform_empty(self, klass): - """Test transform with none_ok = False and an empty value.""" - assert klass().transform('') is None +class TestFormatString: + + @pytest.fixture + def typ(self): + return configtypes.FormatString(fields=('foo', 'bar')) + + @pytest.mark.parametrize('val', [ + 'foo bar baz', + '{foo} {bar} baz', + None, + ]) + def test_from_py_valid(self, typ, val): + typ.none_ok = True + assert typ.from_py(val) == val + + @pytest.mark.parametrize('val', [ + '{foo} {bar} {baz}', + '{foo} {bar', + '{1}', + None, + ]) + def test_from_py_invalid(self, typ, val): + with pytest.raises(configexc.ValidationError): + typ.from_py(val) class TestShellCommand: - """Test ShellCommand.""" - @pytest.fixture def klass(self): return configtypes.ShellCommand - @pytest.mark.parametrize('kwargs, val', [ - ({'none_ok': True}, ''), - ({}, 'foobar'), - ({'placeholder': '{}'}, 'foo {} bar'), - ({'placeholder': '{}'}, 'foo{}bar'), - ({'placeholder': '{}'}, 'foo "bar {}"'), + @pytest.mark.parametrize('kwargs, val, expected', [ + ({'none_ok': True}, '', None), + ({}, 'foobar', ['foobar']), + ({'placeholder': '{}'}, 'foo {} bar', ['foo', '{}', 'bar']), + ({'placeholder': '{}'}, 'foo{}bar', ['foo{}bar']), + ({'placeholder': '{}'}, 'foo "bar {}"', ['foo', 'bar {}']), ]) - def test_validate_valid(self, klass, kwargs, val): - klass(**kwargs).validate(val) + def test_valid(self, klass, kwargs, val, expected): + cmd = klass(**kwargs) + assert cmd.from_str(val) == expected + assert cmd.from_py(expected) == expected @pytest.mark.parametrize('kwargs, val', [ ({}, ''), @@ -1411,39 +1407,36 @@ class TestShellCommand: ({'placeholder': '{}'}, 'foo { } bar'), ({}, 'foo"'), # not splittable with shlex ]) - def test_validate_invalid(self, klass, kwargs, val): + def test_from_str_invalid(self, klass, kwargs, val): with pytest.raises(configexc.ValidationError): - klass(**kwargs).validate(val) - - @pytest.mark.parametrize('val, expected', [ - ('foobar', ['foobar']), - ('foobar baz', ['foobar', 'baz']), - ('foo "bar baz" fish', ['foo', 'bar baz', 'fish']), - ('', None), - ]) - def test_transform_single(self, klass, val, expected): - """Test transform with a single word.""" - assert klass().transform(val) == expected + klass(**kwargs).from_str(val) class TestProxy: - """Test Proxy.""" - @pytest.fixture def klass(self): return configtypes.Proxy - @pytest.mark.parametrize('val', [ - '', - 'system', - 'none', - 'http://user:pass@example.com:2323/', - 'pac+http://example.com/proxy.pac', - 'pac+file:///tmp/proxy.pac' + @pytest.mark.parametrize('val, expected', [ + (None, None), + ('system', configtypes.SYSTEM_PROXY), + ('none', QNetworkProxy(QNetworkProxy.NoProxy)), + ('socks://example.com/', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), + ('socks5://foo:bar@example.com:2323', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, + 'foo', 'bar')), + ('pac+http://example.com/proxy.pac', + pac.PACFetcher(QUrl('pac+http://example.com/proxy.pac'))), + ('pac+file:///tmp/proxy.pac', + pac.PACFetcher(QUrl('pac+file:///tmp/proxy.pac'))), ]) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + def test_from_py_valid(self, klass, val, expected): + actual = klass(none_ok=True).from_py(val) + if isinstance(actual, QNetworkProxy): + actual = QNetworkProxy(actual) + assert actual == expected @pytest.mark.parametrize('val', [ '', @@ -1451,9 +1444,9 @@ class TestProxy: ':', # invalid URL 'ftp://example.com/', # invalid scheme ]) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) + klass().from_py(val) def test_complete(self, klass): """Test complete.""" @@ -1463,87 +1456,9 @@ class TestProxy: ('http://', 'HTTP proxy URL')] assert actual[:3] == expected - @pytest.mark.parametrize('val, expected', [ - ('', None), - ('system', configtypes.SYSTEM_PROXY), - ('none', QNetworkProxy(QNetworkProxy.NoProxy)), - ('socks://example.com/', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), - ('socks5://foo:bar@example.com:2323', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, - 'foo', 'bar')), - ]) - def test_transform(self, klass, val, expected): - """Test transform with an empty value.""" - actual = klass().transform(val) - if isinstance(actual, QNetworkProxy): - actual = QNetworkProxy(actual) - assert actual == expected - - -class TestSearchEngineName: - - """Test SearchEngineName.""" - - @pytest.fixture - def klass(self): - return configtypes.SearchEngineName - - @pytest.mark.parametrize('val', ['', 'foobar']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - def test_validate_empty(self, klass): - with pytest.raises(configexc.ValidationError): - klass().validate('') - - @pytest.mark.parametrize('val, expected', [('', None), - ('foobar', 'foobar')]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - -class TestHeaderDict: - - @pytest.fixture - def klass(self): - return configtypes.HeaderDict - - @pytest.mark.parametrize('val', [ - '{"foo": "bar"}', - '{"foo": "bar", "baz": "fish"}', - '', # empty value with none_ok=true - '{}', # ditto - ]) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', [ - '["foo"]', # valid json but not a dict - '{"hello": 23}', # non-string as value - '{"hällo": "world"}', # non-ascii data in key - '{"hello": "wörld"}', # non-ascii data in value - '', # empty value with none_ok=False - '{}', # ditto - '[invalid', # invalid json - ]) - def test_validate_invalid(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', [ - ('{"foo": "bar"}', {"foo": "bar"}), - ('{}', None), - ('', None), - ]) - def test_transform(self, klass, val, expected): - assert klass(none_ok=True).transform(val) == expected - class TestSearchEngineUrl: - """Test SearchEngineUrl.""" - @pytest.fixture def klass(self): return configtypes.SearchEngineUrl @@ -1552,10 +1467,10 @@ class TestSearchEngineUrl: 'http://example.com/?q={}', 'http://example.com/?q={0}', 'http://example.com/?q={0}&a={0}', - '', # empty value with none_ok + None, # empty value with none_ok ]) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + def test_from_py_valid(self, klass, val): + assert klass(none_ok=True).from_py(val) == val @pytest.mark.parametrize('val', [ '', # empty value without none_ok @@ -1565,269 +1480,154 @@ class TestSearchEngineUrl: '{1}{}', # numbered format string variable '{{}', # invalid format syntax ]) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', [ - ('', None), - ('foobar', 'foobar'), - ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + klass().from_py(val) class TestFuzzyUrl: - """Test FuzzyUrl.""" - @pytest.fixture def klass(self): return configtypes.FuzzyUrl - @pytest.mark.parametrize('val', [ - '', - 'http://example.com/?q={}', - 'example.com', + @pytest.mark.parametrize('val, expected', [ + (None, None), + ('http://example.com/?q={}', QUrl('http://example.com/?q={}')), + ('example.com', QUrl('http://example.com')), ]) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + def test_from_py_valid(self, klass, val, expected): + assert klass(none_ok=True).from_py(val) == expected @pytest.mark.parametrize('val', [ - '', + None, '::foo', # invalid URL 'foo bar', # invalid search term ]) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', [ - ('', None), - ('example.com', QUrl('http://example.com')), - ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + klass().from_py(val) class TestPadding: - """Test Padding.""" - @pytest.fixture def klass(self): return configtypes.Padding - @pytest.mark.parametrize('val', [ - '', - '1,,2,3', - '1,2,3,4', - '1, 2, 3, 4', - '0,0,0,0', + @pytest.mark.parametrize('val, expected', [ + (None, None), + ({'top': 1, 'bottom': 2, 'left': 3, 'right': 4}, + configtypes.PaddingValues(1, 2, 3, 4)), ]) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', [ - '', - '5', - '1,,2,3', - '0.5', - '-1', - '1,2', - '1,2,3', - '1,2,3,4,5', - '1,2,-1,3', - ]) - def test_validate_invalid(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass().validate(val) + def test_from_py_valid(self, klass, val, expected): + assert klass(none_ok=True).from_py(val) == expected @pytest.mark.parametrize('val, expected', [ ('', None), - ('1,2,3,4', (1, 2, 3, 4)), + ('{"top": 1, "bottom": 2, "left": 3, "right": 4}', + configtypes.PaddingValues(1, 2, 3, 4)), ]) - def test_transform(self, klass, val, expected): - """Test transforming of values.""" - transformed = klass().transform(val) - assert transformed == expected - if expected is not None: - assert transformed.top == expected[0] - assert transformed.bottom == expected[1] - assert transformed.left == expected[2] - assert transformed.right == expected[3] + def test_from_str_valid(self, klass, val, expected): + assert klass(none_ok=True).from_str(val) == expected - -class TestAutoSearch: - - """Test AutoSearch.""" - - TESTS = { - 'naive': 'naive', - 'NAIVE': 'naive', - 'dns': 'dns', - 'DNS': 'dns', - '': None, - } - TESTS.update({k: 'naive' for k, v in TestBool.TESTS.items() if v}) - TESTS.update({k: v for k, v in TestBool.TESTS.items() - if not v and v is not None}) - - INVALID = ['ddns', 'foo', ''] - - @pytest.fixture - def klass(self): - return configtypes.AutoSearch - - @pytest.mark.parametrize('val', sorted(TESTS)) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', INVALID) + @pytest.mark.parametrize('val', [ + None, + {'top': 1, 'bottom': 2, 'left': 3, 'right': 4, 'foo': 5}, + {'top': 1, 'bottom': 2, 'left': 3, 'right': 'four'}, + {'top': 1, 'bottom': 2}, + {'top': -1, 'bottom': 2, 'left': 3, 'right': 4}, + {'top': 0.1, 'bottom': 2, 'left': 3, 'right': 4}, + ]) def test_validate_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - -class TestIgnoreCase: - - """Test IgnoreCase.""" - - TESTS = { - 'smart': 'smart', - 'SMART': 'smart', - } - TESTS.update(TestBool.TESTS) - - INVALID = ['ssmart', 'foo'] - - @pytest.fixture - def klass(self): - return configtypes.IgnoreCase - - @pytest.mark.parametrize('val', sorted(TESTS)) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', INVALID) - def test_validate_invalid(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + klass().from_py(val) class TestEncoding: - """Test Encoding.""" - @pytest.fixture def klass(self): return configtypes.Encoding - @pytest.mark.parametrize('val', ['utf-8', 'UTF-8', 'iso8859-1', '']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + @pytest.mark.parametrize('val', ['utf-8', 'UTF-8', 'iso8859-1', None]) + def test_from_py(self, klass, val): + assert klass(none_ok=True).from_py(val) == val @pytest.mark.parametrize('val', ['blubber', '']) def test_validate_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', [('utf-8', 'utf-8'), ('', None)]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + klass().from_py(val) class TestUrl: - """Test Url.""" - TESTS = { 'http://qutebrowser.org/': QUrl('http://qutebrowser.org/'), 'http://heise.de/': QUrl('http://heise.de/'), - '': None, + None: None, } @pytest.fixture def klass(self): return configtypes.Url - @pytest.mark.parametrize('val', sorted(TESTS)) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + @pytest.mark.parametrize('val, expected', list(TESTS.items())) + def test_from_py_valid(self, klass, val, expected): + assert klass(none_ok=True).from_py(val) == expected - @pytest.mark.parametrize('val', ['', '+']) - def test_validate_invalid(self, klass, val): + @pytest.mark.parametrize('val', [None, '+']) + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) - def test_transform_single(self, klass, val, expected): - assert klass().transform(val) == expected + klass().from_py(val) class TestSessionName: - """Test SessionName.""" - @pytest.fixture def klass(self): return configtypes.SessionName - @pytest.mark.parametrize('val', ['', 'foobar']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + @pytest.mark.parametrize('val', [None, 'foobar']) + def test_from_py_valid(self, klass, val): + assert klass(none_ok=True).from_py(val) == val - @pytest.mark.parametrize('val', ['', '_foo']) - def test_validate_invalid(self, klass, val): + @pytest.mark.parametrize('val', [None, '_foo']) + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) + klass().from_py(val) class TestConfirmQuit: - """Test ConfirmQuit.""" - - TESTS = { - '': None, - 'always': ['always'], - 'never': ['never'], - 'multiple-tabs,downloads': ['multiple-tabs', 'downloads'], - 'downloads,multiple-tabs': ['downloads', 'multiple-tabs'], - 'downloads,,multiple-tabs': ['downloads', None, 'multiple-tabs'], - } + TESTS = [ + None, + ['multiple-tabs', 'downloads'], + ['downloads', 'multiple-tabs'], + ['downloads', None, 'multiple-tabs'], + ] @pytest.fixture def klass(self): return configtypes.ConfirmQuit - @pytest.mark.parametrize('val', sorted(TESTS.keys())) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + @pytest.mark.parametrize('val', TESTS) + def test_from_py_valid(self, klass, val): + cq = klass(none_ok=True) + assert cq.from_py(val) == val + assert cq.from_str(json.dumps(val)) == val @pytest.mark.parametrize('val', [ - '', # with none_ok=False - 'foo', - 'downloads,foo', # valid value mixed with invalid one - 'downloads,,multiple-tabs', # empty value - 'downloads,multiple-tabs,downloads', # duplicate value - 'always,downloads', # always combined - 'never,downloads', # never combined + None, # with none_ok=False + ['foo'], + ['downloads', 'foo'], # valid value mixed with invalid one + ['downloads', 'multiple-tabs', 'downloads'], # duplicate value + ['always', 'downloads'], # always combined + ['never', 'downloads'], # never combined ]) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected + klass().from_py(val) def test_complete(self, klass): """Test completing by doing some samples.""" @@ -1842,82 +1642,20 @@ class TestConfirmQuit: assert ',never' not in val -class TestFormatString: - - """Test FormatString.""" - - @pytest.fixture - def typ(self): - return configtypes.FormatString(fields=('foo', 'bar')) - - @pytest.mark.parametrize('val', [ - 'foo bar baz', - '{foo} {bar} baz', - '', - ]) - def test_validate_valid(self, typ, val): - typ.none_ok = True - typ.validate(val) - - @pytest.mark.parametrize('val', [ - '{foo} {bar} {baz}', - '{foo} {bar', - '{1}', - '', - ]) - def test_validate_invalid(self, typ, val): - with pytest.raises(configexc.ValidationError): - typ.validate(val) - - def test_transform(self, typ): - assert typ.transform('foo {bar} baz') == 'foo {bar} baz' - - -class TestUserAgent: - - """Test UserAgent.""" - - @pytest.fixture - def klass(self): - return configtypes.UserAgent - - @pytest.mark.parametrize('val', [ - '', - 'Hello World! :-)', - ]) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', ['', 'überbrowser']) - def test_validate_invalid(self, klass, val): - """Test validate with empty string and none_ok = False.""" - with pytest.raises(configexc.ValidationError): - klass().validate(val) - - def test_transform(self, klass): - assert klass().transform('foobar') == 'foobar' - - def test_complete(self, klass): - """Simple smoke test for completion.""" - klass().complete() - - class TestTimestampTemplate: - """Test TimestampTemplate.""" - @pytest.fixture def klass(self): return configtypes.TimestampTemplate - @pytest.mark.parametrize('val', ['', 'foobar', '%H:%M', 'foo %H bar %M']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + @pytest.mark.parametrize('val', [None, 'foobar', '%H:%M', 'foo %H bar %M']) + def test_from_py_valid(self, klass, val): + assert klass(none_ok=True).from_py(val) == val - @pytest.mark.parametrize('val', ['', '%']) - def test_validate_invalid(self, klass, val): + @pytest.mark.parametrize('val', [None, '%']) + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): - klass().validate(val) + klass().from_py(val) @pytest.mark.parametrize('first, second, equal', [ From 7e7fbf106b512365a5386a2500a059c6dd1769b0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 19:33:47 +0200 Subject: [PATCH 048/516] Fix lint and old config options --- qutebrowser/browser/downloads.py | 8 ++---- qutebrowser/browser/mouse.py | 2 +- qutebrowser/browser/network/pac.py | 1 + qutebrowser/browser/qutescheme.py | 2 +- qutebrowser/browser/shared.py | 4 +-- qutebrowser/browser/webkit/webkitinspector.py | 2 +- qutebrowser/browser/webkit/webpage.py | 2 +- qutebrowser/commands/runners.py | 2 +- qutebrowser/completion/completiondelegate.py | 2 +- qutebrowser/config/configdata.py | 6 ++-- qutebrowser/config/configdata.yml | 23 ++++++++------- qutebrowser/config/configtypes.py | 22 ++++++++------- qutebrowser/config/newconfig.py | 6 ++-- qutebrowser/config/style.py | 2 -- qutebrowser/keyinput/basekeyparser.py | 2 +- qutebrowser/mainwindow/mainwindow.py | 2 +- qutebrowser/mainwindow/prompt.py | 4 +-- qutebrowser/mainwindow/tabwidget.py | 4 +-- qutebrowser/misc/editor.py | 8 +++--- qutebrowser/misc/sessions.py | 2 +- qutebrowser/utils/jinja.py | 2 ++ qutebrowser/utils/urlutils.py | 1 + qutebrowser/utils/utils.py | 2 +- tests/unit/config/test_configdata.py | 4 +-- tests/unit/config/test_configtypes.py | 9 +++--- tests/unit/config/test_style.py | 28 ------------------- 26 files changed, 62 insertions(+), 90 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 9a9c63f13..7b10ef30b 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -69,8 +69,8 @@ class UnsupportedOperationError(Exception): def download_dir(): """Get the download directory to use.""" - directory = config.val.storage.download_directory - remember_dir = config.val.storage.remember_download_directory + directory = config.val.downloads.location.directory + remember_dir = config.val.downloads.location.remember if remember_dir and last_used_directory is not None: return last_used_directory @@ -104,7 +104,7 @@ def _path_suggestion(filename): Args: filename: The filename to use if included in the suggestion. """ - suggestion = config.val.completion.download_path_suggestion + suggestion = config.val.completion.downloads.location.suggestion if suggestion == 'path': # add trailing '/' if not present return os.path.join(download_dir(), '') @@ -472,8 +472,6 @@ class AbstractDownloadItem(QObject): Args: position: The color type requested, can be 'fg' or 'bg'. """ - # pylint: disable=bad-config-call - # WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/ assert position in ["fg", "bg"] start = config.get('colors', 'downloads.{}.start'.format(position)) stop = config.get('colors', 'downloads.{}.stop'.format(position)) diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 51de41105..619f75120 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -119,7 +119,7 @@ class MouseEventFilter(QObject): return True if e.modifiers() & Qt.ControlModifier: - divider = config.val.input.mouse_zoom_divider + divider = config.val.zoom.mouse_divider if divider == 0: return False factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index c6619311c..8f729c9d2 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -253,6 +253,7 @@ class PACFetcher(QObject): self._error_message = None def __eq__(self, other): + # pylint: disable=protected-access return self._pac_url == other._pac_url def __repr__(self): diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 0b60848cc..83056e396 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -278,7 +278,7 @@ def qute_history(url): return 'text/html', json.dumps(history_data(start_time)) else: if ( - config.val.content.allow_javascript and + config.val.content.javascript.enabled and (objects.backend == usertypes.Backend.QtWebEngine or qtutils.is_qtwebkit_ng()) ): diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 6caeafb07..670c9f4f8 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -86,7 +86,7 @@ def javascript_prompt(url, js_msg, default, abort_on): log.js.debug("prompt: {}".format(js_msg)) if config.val.content.javascript.modal_dialog: raise CallSuper - if config.val.content.ignore_javascript_prompt: + if not config.val.content.javascript.prompt: return (False, "") msg = '{} asks:
        {}'.format(html.escape(url.toDisplayString()), @@ -108,7 +108,7 @@ def javascript_alert(url, js_msg, abort_on): if config.val.content.javascript.modal_dialog: raise CallSuper - if config.val.content.ignore_javascript_alert: + if not config.val.content.javascript.alert: return msg = 'From {}:
        {}'.format(html.escape(url.toDisplayString()), diff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py index 2be95cb3d..10049f9b1 100644 --- a/qutebrowser/browser/webkit/webkitinspector.py +++ b/qutebrowser/browser/webkit/webkitinspector.py @@ -36,7 +36,7 @@ class WebKitInspector(inspector.AbstractWebInspector): self._set_widget(qwebinspector) def inspect(self, page): - if not config.val.developer_extras: + if not config.val.content.developer_extras: raise inspector.WebInspectorError( "Please enable developer-extras before using the " "webinspector!") diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index b7a8fe2a5..70750eef8 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -277,7 +277,7 @@ class BrowserPage(QWebPage): reply.finished.connect(functools.partial( self.display_content, reply, 'image/jpeg')) elif (mimetype in ['application/pdf', 'application/x-pdf'] and - config.val.content.enable_pdfjs): + config.val.content.pdfjs): # Use pdf.js to display the page self._show_pdfjs(reply) else: diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 79da78fbd..5dce593e5 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -25,7 +25,7 @@ import re from PyQt5.QtCore import pyqtSlot, QUrl, QObject -from qutebrowser.config import config, configexc +from qutebrowser.config import config from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.misc import split diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index af4141d4d..19b655ac7 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import QRectF, QSize, Qt from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption, QAbstractTextDocumentLayout) -from qutebrowser.config import config, configexc, style +from qutebrowser.config import config, style from qutebrowser.utils import qtutils diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 0ef0d1e25..7d36a499a 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -33,7 +33,7 @@ import sys import re import collections -from qutebrowser.config import configtypes, sections +from qutebrowser.config import configtypes from qutebrowser.utils import usertypes, qtutils, utils DATA = None @@ -601,8 +601,8 @@ def _parse_yaml_backends_dict(name, node): backends = [] - # The value associated to the key, and whether we should add that backend or - # not. + # The value associated to the key, and whether we should add that backend + # or not. conditionals = { True: True, False: False, diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index f7c3aa7d7..220304799 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -536,16 +536,6 @@ completion.show: - never: Never. desc: When to show the autocompletion window. -downloads.path_suggestion: - default: path - type: - name: String - valid_values: - - path: Show only the download path. - - filename: Show only download filename. - - both: Show download path and filename. - desc: What to display in the download filename input. - completion.timestamp_format: type: name: TimestampTemplate @@ -909,6 +899,17 @@ downloads.location.remember: type: Bool desc: Whether to remember the last used download directory. +downloads.location.suggestion: + default: path + type: + name: String + valid_values: + - path: Show only the download path. + - filename: Show only download filename. + - both: Show download path and filename. + desc: What to display in the download filename input. + + # Defaults from QWebSettings::QWebSettings() in # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp @@ -1020,13 +1021,11 @@ content.javascript.can_access_clipboard: allowed. content.javascript.prompt: - # FIXME:conf meaning changed! default: true type: Bool desc: Show javascript prompts. content.javascript.alert: - # FIXME:conf meaning changed! default: false type: Bool desc: Show javascript. diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 3fd1b1869..85fa6a07d 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -161,8 +161,8 @@ class BaseType: """Get the setting value from a string. By default this tries to invoke from_py(), so if from_py() accepts a - string rather than something more sophisticated, this doesn't need to be - implemented. + string rather than something more sophisticated, this doesn't need to + be implemented. Args: value: The original string value. @@ -484,21 +484,21 @@ class BoolAsk(Bool): self.valid_values = ValidValues('true', 'false', 'ask') def from_py(self, value): - # basic validation unneeded if it's == 'ask' and done by Bool if we call - # super().from_py + # basic validation unneeded if it's == 'ask' and done by Bool if we + # call super().from_py if isinstance(value, str) and value.lower() == 'ask': return 'ask' return super().from_py(value) def from_str(self, value): - # basic validation unneeded if it's == 'ask' and done by Bool if we call - # super().from_str + # basic validation unneeded if it's == 'ask' and done by Bool if we + # call super().from_str if isinstance(value, str) and value.lower() == 'ask': return 'ask' return super().from_str(value) -class _Numeric(BaseType): +class _Numeric(BaseType): # pylint: disable=abstract-method """Base class for Float/Int. @@ -658,8 +658,8 @@ class PercOrInt(_Numeric): raise configexc.ValidationError(value, "must be {}% or " "less!".format(self.maxperc)) - # Note we don't actually return the integer here, as we need to know - # whether it was a percentage. + # Note we don't actually return the integer here, as we need to + # know whether it was a percentage. else: self._validate_bounds(value) return value @@ -770,7 +770,9 @@ class Font(BaseType): if not value: return None - if not self.font_regex.match(value): # FIXME:conf this used to have "pragma: no cover" + if not self.font_regex.match(value): # pragma: no cover + # This should never happen, as the regex always matches everything + # as family. raise configexc.ValidationError(value, "must be a valid font") return value diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index c06eee55d..efe00d0d2 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -138,10 +138,10 @@ class NewConfigManager(QObject): def get(self, option): try: - val = self._values[option] - except KeyError as e: + value = self._values[option] + except KeyError: raise configexc.NoOptionError(option) - return val.typ.from_py(val.default) + return value.typ.from_py(value.default) class ConfigContainer: diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index e6d5333e5..1652bf52d 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -20,10 +20,8 @@ """Utilities related to the look&feel of qutebrowser.""" import functools -import collections import sip -from PyQt5.QtGui import QColor from qutebrowser.config import config from qutebrowser.utils import log, objreg, jinja diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 897abb841..46a506432 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -271,7 +271,7 @@ class BaseKeyParser(QObject): count: The count to pass. """ self._debug_log("Ambiguous match for '{}'".format(self._keystring)) - time = config.val.input.timeout + time = config.val.input.ambiguous_timeout if time == 0: # execute immediately self.clear_keystring() diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 5df4aedef..8f1b842d7 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -98,7 +98,7 @@ def get_window(via_ipc, force_window=False, force_tab=False, def get_target_window(): """Get the target window for new tabs, or None if none exist.""" try: - win_mode = config.val.new_instance_open_target.window + win_mode = config.val.new_instance_open_target_window if win_mode == 'last-focused': return objreg.last_focused_window() elif win_mode == 'first-opened': diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index d6deaa8e7..68f6f09b4 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -565,7 +565,7 @@ class FilenamePrompt(_BasePrompt): self.setFocusProxy(self._lineedit) self._init_key_label() - if config.val.ui.prompt.filebrowser: + if config.val.prompt.filebrowser: self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @pyqtSlot(str) @@ -627,7 +627,7 @@ class FilenamePrompt(_BasePrompt): self._file_view.setModel(self._file_model) self._file_view.clicked.connect(self._insert_path) - if config.val.ui.prompt.filebrowser: + if config.val.prompt.filebrowser: self._vbox.addWidget(self._file_view) else: self._file_view.hide() diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 6034a3ce1..3081c7e41 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -484,7 +484,7 @@ class TabBar(QTabBar): minimum_size = self.minimumTabSizeHint(index) height = minimum_size.height() if self.vertical: - confwidth = str(config.val.tabs.width) + confwidth = str(config.val.tabs.width.bar) if confwidth.endswith('%'): main_window = objreg.get('main-window', scope='window', window=self._win_id) @@ -593,7 +593,7 @@ class TabBar(QTabBar): Args: e: The QWheelEvent """ - if config.val.tabs.mousewheel_tab_switching: + if config.val.tabs.mousewheel_switching: super().wheelEvent(e) else: tabbed_browser = objreg.get('tabbed-browser', scope='window', diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index dba12d529..524588b54 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -75,7 +75,7 @@ class ExternalEditor(QObject): try: if exitcode != 0: return - encoding = config.val.editor_encoding + encoding = config.val.editor.encoding try: with open(self._file.name, 'r', encoding=encoding) as f: text = f.read() @@ -102,14 +102,14 @@ class ExternalEditor(QObject): if self._text is not None: raise ValueError("Already editing a file!") self._text = text - encoding = config.val.editor_encoding try: # Close while the external process is running, as otherwise systems # with exclusive write access (e.g. Windows) may fail to update # the file from the external editor, see # https://github.com/qutebrowser/qutebrowser/issues/1767 with tempfile.NamedTemporaryFile( - mode='w', prefix='qutebrowser-editor-', encoding=encoding, + mode='w', prefix='qutebrowser-editor-', + encoding=config.val.editor.encoding, delete=False) as fobj: if text: fobj.write(text) @@ -120,7 +120,7 @@ class ExternalEditor(QObject): self._proc = guiprocess.GUIProcess(what='editor', parent=self) self._proc.finished.connect(self.on_proc_closed) self._proc.error.connect(self.on_proc_error) - editor = config.val.editor + editor = config.val.editor.command executable = editor[0] args = [arg.replace('{}', self._file.name) for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index c9abfc830..3768d53cd 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -382,7 +382,7 @@ class SessionManager(QObject): path = self._get_session_path(name, check_exists=True) try: with open(path, encoding='utf-8') as f: - data = utils.yaml_load(f, Loader=YamlLoader) + data = utils.yaml_load(f) except (OSError, UnicodeDecodeError, yaml.YAMLError) as e: raise SessionError(e) diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index e2eab4542..f6d9499a5 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -78,6 +78,8 @@ class Loader(jinja2.BaseLoader): class Environment(jinja2.Environment): + """Our own jinja environment which is more strict.""" + def __init__(self): super().__init__(loader=Loader('html'), autoescape=self._guess_autoescape, diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 7ba457e2d..02317d2cb 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -99,6 +99,7 @@ def _get_search_url(txt): engine, term = _parse_search_term(txt) assert term if engine is None: + # FIXME:conf template = config.val.searchengines.DEFAULT else: template = config.get('searchengines', engine) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index fbafa5353..95d3c9468 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -841,7 +841,7 @@ def open_file(filename, cmdline=None): from qutebrowser.config import config # the default program to open downloads with - will be empty string # if we want to use the default - override = config.val.default_open_dispatcher + override = config.val.downloads.open_dispatcher # precedence order: cmdline > default-open-dispatcher > openUrl diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py index 87231f82f..c85ddf47c 100644 --- a/tests/unit/config/test_configdata.py +++ b/tests/unit/config/test_configdata.py @@ -195,8 +195,8 @@ class TestParseYamlBackend: @pytest.mark.parametrize('backend, expected', [ ('QtWebKit', [usertypes.Backend.QtWebKit]), ('QtWebEngine', [usertypes.Backend.QtWebEngine]), - # This is also what _parse_yaml_backends gets when backend: is not given - # at all + # This is also what _parse_yaml_backends gets when backend: is not + # given at all ('null', [usertypes.Backend.QtWebKit, usertypes.Backend.QtWebEngine]), ]) def test_simple(self, backend, expected): diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index fc83697fa..61067f0e9 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -22,7 +22,6 @@ import re import json import collections import itertools -import os.path import warnings import pytest @@ -796,7 +795,7 @@ class TestPercOrInt: ({'minint': 2, 'maxint': 3}, '4%', '4%'), ]) def test_from_str_valid(self, klass, kwargs, val, expected): - klass(**kwargs).from_str(val) == expected + assert klass(**kwargs).from_str(val) == expected @pytest.mark.parametrize('kwargs, val', [ ({}, '1337%%'), @@ -820,7 +819,7 @@ class TestPercOrInt: @pytest.mark.parametrize('val', ['1337%', 1337, None]) def test_from_py_valid(self, klass, val): - klass(none_ok=True).from_py(val) == val + assert klass(none_ok=True).from_py(val) == val @pytest.mark.parametrize('val', ['1337%%', '1337']) def test_from_py_invalid(self, klass, val): @@ -846,7 +845,7 @@ class TestCommand: 'cmd2 baz fish']) def test_from_py_valid(self, klass, val): expected = None if not val else val - klass(none_ok=True).from_py(val) == expected + assert klass(none_ok=True).from_py(val) == expected @pytest.mark.parametrize('val', ['', 'cmd3', 'cmd3 foo bar', ' ']) def test_from_py_invalid(self, klass, val): @@ -1090,7 +1089,7 @@ class TestFontFamily: @pytest.mark.parametrize('val', TESTS) def test_from_py_valid(self, klass, val): - klass(none_ok=True).from_py(val) == val + assert klass(none_ok=True).from_py(val) == val @pytest.mark.parametrize('val', INVALID) def test_from_py_invalid(self, klass, val): diff --git a/tests/unit/config/test_style.py b/tests/unit/config/test_style.py index 2e4d8c1ce..36c811b65 100644 --- a/tests/unit/config/test_style.py +++ b/tests/unit/config/test_style.py @@ -85,31 +85,3 @@ def test_set_register_stylesheet(delete, qtbot, config_stub, caplog): else: expected = 'baz' assert obj.rendered_stylesheet == expected - - -class TestColorDict: - - @pytest.mark.parametrize('key, expected', [ - ('foo', 'one'), - ('foo.fg', 'two'), - ('foo.bg', 'three'), - ]) - def test_values(self, key, expected): - d = style.ColorDict() - d['foo'] = 'one' - d['foo.fg'] = 'two' - d['foo.bg'] = 'three' - assert d[key] == expected - - def test_key_error(self, caplog): - d = style.ColorDict() - with caplog.at_level(logging.ERROR): - d['foo'] # pylint: disable=pointless-statement - assert len(caplog.records) == 1 - assert caplog.records[0].message == 'No color defined for foo!' - - def test_qcolor(self): - d = style.ColorDict() - d['foo'] = QColor() - with pytest.raises(TypeError): - d['foo'] # pylint: disable=pointless-statement From 52f15c84a68a7bc368ba2629d43c9031ffcaf0da Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 19:34:36 +0200 Subject: [PATCH 049/516] Get rid of config sections --- qutebrowser/config/config.py | 6 ------ qutebrowser/config/newconfig.py | 12 ------------ qutebrowser/config/parsers/keyconf.py | 7 ++++--- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index a51fda016..8afb35813 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -63,12 +63,6 @@ def get(*args, **kwargs): return objreg.get('config').get(*args, **kwargs) -def section(sect): - """Get a config section from the global config.""" - config = objreg.get('config') - return newconfig.SectionStub(config, sect) - - def _init_main_config(parent=None): """Initialize the main config. diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index efe00d0d2..475ac69a9 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -33,18 +33,6 @@ instance = None _change_filters = [] -class SectionStub: - - # FIXME get rid of this once we get rid of sections - - def __init__(self, conf, name): - self._conf = conf - self._name = name - - def __getitem__(self, item): - return self._conf.get(self._name, item) - - class change_filter: # pylint: disable=invalid-name """Decorator to filter calls based on a config section/option matching. diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index 53f23d7c0..216715ddb 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -363,11 +363,12 @@ class KeyConfigParser(QObject): line)) commands = [c.split(maxsplit=1)[0].strip() for c in commands] for cmd in commands: - aliases = config.section('aliases') + # FIXME:conf + # aliases = config.section('aliases') if cmd in cmdutils.cmd_dict: cmdname = cmd - elif cmd in aliases: - cmdname = aliases[cmd].split(maxsplit=1)[0].strip() + # elif cmd in aliases: + # cmdname = aliases[cmd].split(maxsplit=1)[0].strip() else: raise KeyConfigError("Invalid command '{}'!".format(cmd)) cmd_obj = cmdutils.cmd_dict[cmdname] From 6a451b37d73dc84f437eea7947bf1732b551abd1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 19:46:37 +0200 Subject: [PATCH 050/516] re-raise NoOptionError --- qutebrowser/config/newconfig.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/newconfig.py b/qutebrowser/config/newconfig.py index 475ac69a9..b07e1abc9 100644 --- a/qutebrowser/config/newconfig.py +++ b/qutebrowser/config/newconfig.py @@ -161,8 +161,11 @@ class ConfigContainer: name = self._join(attr) if configdata.is_valid_prefix(name): return ConfigContainer(manager=self._manager, prefix=name) - # If it's not a valid prefix, this will raise NoOptionError. - return self._manager.get(name) + try: + return self._manager.get(name) + except configexc.NoOptionError as e: + # If it's not a valid prefix - re-raise to improve error text. + raise configexc.NoOptionError(name) def __setattr__(self, attr, value): if attr.startswith('_'): From cc0e66fe7b95f189ff7fceafe69d10846c0a909d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 19:58:56 +0200 Subject: [PATCH 051/516] More config fixes --- qutebrowser/config/configdata.yml | 4 ++-- qutebrowser/utils/urlutils.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 220304799..c7fc8b2e7 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -364,9 +364,9 @@ window.hide_wayland_decoration: keyhint.blacklist: type: name: List + none_ok: true valtype: name: String - none_ok: true default: null desc: >- Keychains that shouldn\'t be shown in the keyhint dialog. @@ -1526,7 +1526,7 @@ colors.downloads.start.bg: desc: Color gradient start for download backgrounds. colors.downloads.stop.fg: - default: '#0000aa' + default: white type: QtColor desc: Color gradient end for download text. diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 02317d2cb..e79be83e1 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -99,10 +99,8 @@ def _get_search_url(txt): engine, term = _parse_search_term(txt) assert term if engine is None: - # FIXME:conf - template = config.val.searchengines.DEFAULT - else: - template = config.get('searchengines', engine) + engine = 'DEFAULT' + template = config.val.searchengines[engine] url = qurl_from_user_input(template.format(urllib.parse.quote(term))) qtutils.ensure_valid(url) return url From 51474724e55c801e0fcd7277343e9af9be1b6d2b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 21:42:36 +0200 Subject: [PATCH 052/516] Refactor objreg.get('config') calls --- qutebrowser/browser/adblock.py | 6 +- qutebrowser/browser/browsertab.py | 2 +- .../browser/webengine/webenginesettings.py | 6 +- qutebrowser/browser/webengine/webview.py | 5 +- qutebrowser/browser/webkit/cache.py | 4 +- qutebrowser/browser/webkit/cookies.py | 4 +- .../webkit/network/webkitqutescheme.py | 1 + qutebrowser/browser/webkit/webkitsettings.py | 6 +- qutebrowser/browser/webkit/webview.py | 2 +- qutebrowser/completion/completionwidget.py | 6 +- qutebrowser/completion/models/configmodel.py | 10 +-- qutebrowser/completion/models/instances.py | 2 +- qutebrowser/completion/models/urlmodel.py | 4 +- qutebrowser/config/config.py | 6 +- qutebrowser/config/style.py | 5 +- qutebrowser/mainwindow/mainwindow.py | 4 +- qutebrowser/mainwindow/messageview.py | 4 +- qutebrowser/mainwindow/statusbar/bar.py | 2 +- qutebrowser/mainwindow/tabbedbrowser.py | 39 +++++---- qutebrowser/mainwindow/tabwidget.py | 79 +++++++++---------- qutebrowser/misc/consolewidget.py | 12 +-- qutebrowser/misc/crashdialog.py | 2 + qutebrowser/misc/lineparser.py | 6 +- qutebrowser/misc/savemanager.py | 8 +- qutebrowser/misc/utilcmds.py | 1 + 25 files changed, 113 insertions(+), 113 deletions(-) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 465cc0b81..eff0a430a 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -114,12 +114,12 @@ class HostBlocker: data_dir = standarddir.data() self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts') - self.on_config_changed() + self._update_files() config_dir = standarddir.config() self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts') - objreg.get('config').changed.connect(self.on_config_changed) + config.instance.changed.connect(self._update_files) def is_blocked(self, url): """Check if the given URL (as QUrl) is blocked.""" @@ -293,7 +293,7 @@ class HostBlocker: len(self._blocked_hosts), self._done_count)) @config.change_filter('content.host_blocking.lists') - def on_config_changed(self): + def _update_files(self): """Update files when the config changed.""" if config.val.content.host_blocking.lists is None: try: diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 81cf190a0..fcad1360e 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -251,7 +251,7 @@ class AbstractZoom(QObject): self._win_id = win_id self._default_zoom_changed = False self._init_neighborlist() - objreg.get('config').changed.connect(self._on_config_changed) + config.instance.changed.connect(self._on_config_changed) # # FIXME:qtwebengine is this needed? # # For some reason, this signal doesn't get disconnected automatically diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index c940eb508..3552a55c2 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -36,7 +36,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, from qutebrowser.browser import shared from qutebrowser.config import config, websettings -from qutebrowser.utils import objreg, utils, standarddir, javascript, qtutils +from qutebrowser.utils import utils, standarddir, javascript, qtutils # The default QWebEngineProfile @@ -166,7 +166,7 @@ def _set_user_agent(profile): profile.setHttpUserAgent(config.val.content.user_agent) -def update_settings(option): +def _update_settings(option): """Update global settings when qwebsettings changed.""" websettings.update_mappings(MAPPINGS, option) if option in ['scrollbar.hide', 'content.user_stylesheet']: @@ -215,7 +215,7 @@ def init(args): Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) websettings.init_mappings(MAPPINGS) - objreg.get('config').changed.connect(update_settings) + config.instance.changed.connect(_update_settings) def shutdown(): diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index bb160d1a2..fcad18206 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -28,8 +28,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage from qutebrowser.browser import shared from qutebrowser.browser.webengine import certificateerror, webenginesettings from qutebrowser.config import config -from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message, - objreg) +from qutebrowser.utils import log, debug, usertypes, jinja, urlutils, message class WebEngineView(QWebEngineView): @@ -135,7 +134,7 @@ class WebEnginePage(QWebEnginePage): self._on_feature_permission_requested) self._theme_color = theme_color self._set_bg_color() - objreg.get('config').changed.connect(self._set_bg_color) + config.instance.changed.connect(self._set_bg_color) @config.change_filter('colors.webpage.bg') def _set_bg_color(self): diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 2a916a0b4..060b6117c 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -24,7 +24,7 @@ import os.path from PyQt5.QtNetwork import QNetworkDiskCache from qutebrowser.config import config -from qutebrowser.utils import utils, objreg, qtutils +from qutebrowser.utils import utils, qtutils class DiskCache(QNetworkDiskCache): @@ -35,7 +35,7 @@ class DiskCache(QNetworkDiskCache): super().__init__(parent) self.setCacheDirectory(os.path.join(cache_dir, 'http')) self._set_cache_size() - objreg.get('config').changed.connect(self._set_cache_size) + config.instance.changed.connect(self._set_cache_size) def __repr__(self): return utils.get_repr(self, size=self.cacheSize(), diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py index 9f2eb8d5e..15d86e03d 100644 --- a/qutebrowser/browser/webkit/cookies.py +++ b/qutebrowser/browser/webkit/cookies.py @@ -74,7 +74,7 @@ class CookieJar(RAMCookieJar): self._lineparser = lineparser.LineParser( standarddir.data(), 'cookies', binary=True, parent=self) self.parse_cookies() - objreg.get('config').changed.connect(self.cookies_store_changed) + config.instance.changed.connect(self._on_cookies_store_changed) objreg.get('save-manager').add_saveable( 'cookies', self.save, self.changed, config_opt='content.cookies.store') @@ -106,7 +106,7 @@ class CookieJar(RAMCookieJar): self._lineparser.save() @config.change_filter('content.cookies.store') - def cookies_store_changed(self): + def _on_cookies_store_changed(self): """Delete stored cookies if cookies-store changed.""" if not config.val.content.cookies.store: self._lineparser.data = [] diff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py index 6e83e60a0..794b5b020 100644 --- a/qutebrowser/browser/webkit/network/webkitqutescheme.py +++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py @@ -83,6 +83,7 @@ class JSBridge(QObject): message.error("Refusing to disable javascript via qute://settings " "as it needs javascript support.") return + # FIXME:conf try: objreg.get('config').set('conf', sectname, optname, value) except (configexc.Error, configparser.Error) as e: diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 769d692bc..f7f94b3fb 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -33,7 +33,7 @@ from PyQt5.QtGui import QFont from PyQt5.QtWebKit import QWebSettings from qutebrowser.config import config, websettings -from qutebrowser.utils import standarddir, objreg, urlutils, qtutils +from qutebrowser.utils import standarddir, urlutils, qtutils from qutebrowser.browser import shared @@ -111,7 +111,7 @@ def _set_user_stylesheet(): QWebSettings.globalSettings().setUserStyleSheetUrl(url) -def update_settings(option): +def _update_settings(option): """Update global settings when qwebsettings changed.""" if option in ['scrollbar.hide', 'content.user_stylesheet']: _set_user_stylesheet() @@ -140,7 +140,7 @@ def init(_args): websettings.init_mappings(MAPPINGS) _set_user_stylesheet() - objreg.get('config').changed.connect(update_settings) + config.instance.changed.connect(_update_settings) def shutdown(): diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 682f8b55f..c3f9abd0e 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -88,7 +88,7 @@ class WebView(QWebView): window=win_id) mode_manager.entered.connect(self.on_mode_entered) mode_manager.left.connect(self.on_mode_left) - objreg.get('config').changed.connect(self._set_bg_color) + config.instance.changed.connect(self._set_bg_color) def __repr__(self): url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 62711aa5f..f19cb8500 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -29,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from qutebrowser.config import config, style from qutebrowser.completion import completiondelegate from qutebrowser.completion.models import base -from qutebrowser.utils import utils, usertypes, objreg +from qutebrowser.utils import utils, usertypes from qutebrowser.commands import cmdexc, cmdutils @@ -109,8 +109,8 @@ class CompletionView(QTreeView): super().__init__(parent) self._win_id = win_id # FIXME handle new aliases. - # objreg.get('config').changed.connect(self.init_command_completion) - objreg.get('config').changed.connect(self._on_config_changed) + # config.instance.changed.connect(self.init_command_completion) + config.instance.changed.connect(self._on_config_changed) self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS self._active = False diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index c9e9850d1..b13bcee8b 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -22,7 +22,7 @@ from PyQt5.QtCore import pyqtSlot, Qt from qutebrowser.config import config, configdata -from qutebrowser.utils import log, qtutils, objreg +from qutebrowser.utils import log, qtutils from qutebrowser.completion.models import base @@ -63,7 +63,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel): sectdata = configdata.DATA[section] self._misc_items = {} self._section = section - objreg.get('config').changed.connect(self.update_misc_column) + config.instance.changed.connect(self._update_misc_column) for name in sectdata: try: desc = sectdata.descriptions[name] @@ -79,7 +79,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel): self._misc_items[name] = miscitem @pyqtSlot(str, str) - def update_misc_column(self, section, option): + def _update_misc_column(self, section, option): """Update misc column when config changed.""" if section != self._section: return @@ -117,7 +117,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel): super().__init__(parent) self._section = section self._option = option - objreg.get('config').changed.connect(self.update_current_value) + config.instance.changed.connect(self._update_current_value) cur_cat = self.new_category("Current/Default", sort=0) value = config.get(section, option, raw=True) if not value: @@ -143,7 +143,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel): self.new_item(cat, val, desc) @pyqtSlot(str, str) - def update_current_value(self, section, option): + def _update_current_value(self, section, option): """Update current value when config changed.""" if (section, option) != (self._section, self._option): return diff --git a/qutebrowser/completion/models/instances.py b/qutebrowser/completion/models/instances.py index f7eaaca86..c686e7fa5 100644 --- a/qutebrowser/completion/models/instances.py +++ b/qutebrowser/completion/models/instances.py @@ -193,4 +193,4 @@ def init(): keyconf.changed.connect( functools.partial(update, [usertypes.Completion.bind])) - objreg.get('config').changed.connect(_update_aliases) + config.instance.changed.connect(_update_aliases) diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index 5409e651b..3069b5674 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -76,7 +76,7 @@ class UrlCompletionModel(base.BaseCompletionModel): self._history.add_completion_item.connect(self.on_history_item_added) self._history.cleared.connect(self.on_history_cleared) - objreg.get('config').changed.connect(self.reformat_timestamps) + config.instance.changed.connect(self._reformat_timestamps) def _fmt_atime(self, atime): """Format an atime to a human-readable string.""" @@ -108,7 +108,7 @@ class UrlCompletionModel(base.BaseCompletionModel): self._remove_oldest_history() @config.change_filter('completion.timestamp_format') - def reformat_timestamps(self): + def _reformat_timestamps(self): """Reformat the timestamps if the config option was changed.""" for i in range(self._history_cat.rowCount()): url_item = self._history_cat.child(i, self.URL_COLUMN) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 8afb35813..569bd27d2 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -60,7 +60,7 @@ instance = None def get(*args, **kwargs): """Convenience method to call get(...) of the config instance.""" - return objreg.get('config').get(*args, **kwargs) + return instance.get(*args, **kwargs) def _init_main_config(parent=None): @@ -89,7 +89,7 @@ def _init_main_config(parent=None): sys.exit(usertypes.Exit.err_config) else: objreg.register('config', config_obj) - filename = os.path.join(standarddir.config(), 'qutebrowser.conf') + save_manager = objreg.get('save-manager') save_manager.add_saveable( 'config', config_obj.save, config_obj.changed, @@ -153,7 +153,7 @@ def _init_misc(): command_history = lineparser.LimitLineParser( standarddir.data(), 'cmd-history', limit='completion.cmd_history_max_items', - parent=objreg.get('config')) + parent=instance) objreg.register('command-history', command_history) save_manager.add_saveable('command-history', command_history.save, command_history.changed) diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index 1652bf52d..de0dfc429 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -24,7 +24,7 @@ import functools import sip from qutebrowser.config import config -from qutebrowser.utils import log, objreg, jinja +from qutebrowser.utils import log, jinja @functools.lru_cache(maxsize=16) @@ -54,8 +54,7 @@ def set_register_stylesheet(obj): log.config.vdebug("stylesheet for {}: {}".format( obj.__class__.__name__, qss)) obj.setStyleSheet(qss) - objreg.get('config').changed.connect( - functools.partial(_update_stylesheet, obj)) + config.instance.changed.connect(functools.partial(_update_stylesheet, obj)) def _update_stylesheet(obj): diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 8f1b842d7..30a3caca1 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -213,7 +213,7 @@ class MainWindow(QWidget): # resizing will fail. Therefore, we use singleShot QTimers to make sure # we defer this until everything else is initialized. QTimer.singleShot(0, self._connect_overlay_signals) - objreg.get('config').changed.connect(self.on_config_changed) + config.instance.changed.connect(self._on_config_changed) objreg.get("app").new_window.emit(self) @@ -324,7 +324,7 @@ class MainWindow(QWidget): return utils.get_repr(self) @pyqtSlot(str) - def on_config_changed(self, option): + def _on_config_changed(self, option): """Resize the completion if related config options changed.""" if option == 'statusbar.padding': self._update_overlay_geometries() diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 122b430f6..370028105 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -24,7 +24,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy from qutebrowser.config import config, style -from qutebrowser.utils import usertypes, objreg +from qutebrowser.utils import usertypes class Message(QLabel): @@ -84,7 +84,7 @@ class MessageView(QWidget): self._clear_timer = QTimer() self._clear_timer.timeout.connect(self.clear_messages) self._set_clear_timer_interval() - objreg.get('config').changed.connect(self._set_clear_timer_interval) + config.instance.changed.connect(self._set_clear_timer_interval) self._last_text = None self._messages = [] diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index dcabdc6ab..2a0c38a75 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -193,7 +193,7 @@ class StatusBar(QWidget): self.prog = progress.Progress(self) self._hbox.addWidget(self.prog) - objreg.get('config').changed.connect(self._on_config_changed) + config.instance.changed.connect(self._on_config_changed) QTimer.singleShot(0, self.maybe_hide) def __repr__(self): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 81025570c..dfa5ed277 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -122,13 +122,20 @@ class TabbedBrowser(tabwidget.TabWidget): self._global_marks = {} self.default_window_icon = self.window().windowIcon() self.private = private - objreg.get('config').changed.connect(self.update_favicons) - objreg.get('config').changed.connect(self.update_window_title) - objreg.get('config').changed.connect(self.update_tab_titles) + config.instance.changed.connect(self._on_config_changed) def __repr__(self): return utils.get_repr(self, count=self.count()) + @pyqtSlot(str) + def _on_config_changed(self, option): + if option == 'tabs.favicons.show': + self._update_favicons() + elif option == 'window.title_format': + self._update_window_title() + elif option in ['tabs.title.format', 'tabs.title.format_pinned']: + self._update_tab_titles() + def _tab_index(self, tab): """Get the index of a given tab. @@ -159,8 +166,7 @@ class TabbedBrowser(tabwidget.TabWidget): widgets.append(widget) return widgets - @config.change_filter('window.title_format') - def update_window_title(self): + def _update_window_title(self): """Change the window title to match the current tab.""" idx = self.currentIndex() if idx == -1: @@ -485,8 +491,7 @@ class TabbedBrowser(tabwidget.TabWidget): self._tab_insert_idx_right)) return idx - @config.change_filter('tabs.favicons.show') - def update_favicons(self): + def _update_favicons(self): """Update favicons when config was changed.""" for i, tab in enumerate(self.widgets()): if config.val.tabs.favicons.show: @@ -510,7 +515,7 @@ class TabbedBrowser(tabwidget.TabWidget): except TabDeletedError: # We can get signals for tabs we already deleted... return - self.update_tab_title(idx) + self._update_tab_title(idx) if tab.data.keep_icon: tab.data.keep_icon = False else: @@ -519,7 +524,7 @@ class TabbedBrowser(tabwidget.TabWidget): config.val.tabs.favicons.show): self.window().setWindowIcon(self.default_window_icon) if idx == self.currentIndex(): - self.update_window_title() + self._update_window_title() @pyqtSlot() def on_cur_load_started(self): @@ -551,7 +556,7 @@ class TabbedBrowser(tabwidget.TabWidget): idx, text)) self.set_page_title(idx, text) if idx == self.currentIndex(): - self.update_window_title() + self._update_window_title() @pyqtSlot(browsertab.AbstractTab, QUrl) def on_url_changed(self, tab, url): @@ -625,7 +630,7 @@ class TabbedBrowser(tabwidget.TabWidget): scope='window', window=self._win_id) self._now_focused = tab self.current_tab_changed.emit(tab) - QTimer.singleShot(0, self.update_window_title) + QTimer.singleShot(0, self._update_window_title) self._tab_insert_idx_left = self.currentIndex() self._tab_insert_idx_right = self.currentIndex() + 1 @@ -646,9 +651,9 @@ class TabbedBrowser(tabwidget.TabWidget): system = config.val.colors.tabs.indicator.system color = utils.interpolate_color(start, stop, perc, system) self.set_tab_indicator_color(idx, color) - self.update_tab_title(idx) + self._update_tab_title(idx) if idx == self.currentIndex(): - self.update_window_title() + self._update_window_title() def on_load_finished(self, tab, ok): """Adjust tab indicator when loading finished.""" @@ -665,9 +670,9 @@ class TabbedBrowser(tabwidget.TabWidget): else: color = config.val.colors.tabs.indicator.error self.set_tab_indicator_color(idx, color) - self.update_tab_title(idx) + self._update_tab_title(idx) if idx == self.currentIndex(): - self.update_window_title() + self._update_window_title() @pyqtSlot() def on_scroll_pos_changed(self): @@ -678,8 +683,8 @@ class TabbedBrowser(tabwidget.TabWidget): log.webview.debug("Not updating scroll position because index is " "-1") return - self.update_window_title() - self.update_tab_title(idx) + self._update_window_title() + self._update_tab_title(idx) def _on_renderer_process_terminated(self, tab, status, code): """Show an error when a renderer process terminated.""" diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 3081c7e41..ea0463896 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -57,18 +57,18 @@ class TabWidget(QTabWidget): self.setTabBar(bar) bar.tabCloseRequested.connect(self.tabCloseRequested) bar.tabMoved.connect(functools.partial( - QTimer.singleShot, 0, self.update_tab_titles)) + QTimer.singleShot, 0, self._update_tab_titles)) bar.currentChanged.connect(self._on_current_changed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setDocumentMode(True) self.setElideMode(Qt.ElideRight) self.setUsesScrollButtons(True) bar.setDrawBase(False) - self.init_config() - objreg.get('config').changed.connect(self.init_config) + self._init_config() + config.instance.changed.connect(self._init_config) @config.change_filter('tabs') - def init_config(self): + def _init_config(self): """Initialize attributes based on the config.""" if self is None: # pragma: no cover # WORKAROUND for PyQt 5.2 @@ -116,7 +116,7 @@ class TabWidget(QTabWidget): bar.set_tab_data(idx, 'pinned', pinned) tab.data.pinned = pinned - self.update_tab_title(idx) + self._update_tab_title(idx) bar.refresh() @@ -127,13 +127,13 @@ class TabWidget(QTabWidget): def set_page_title(self, idx, title): """Set the tab title user data.""" self.tabBar().set_tab_data(idx, 'page-title', title) - self.update_tab_title(idx) + self._update_tab_title(idx) def page_title(self, idx): """Get the tab title user data.""" return self.tabBar().page_title(idx) - def update_tab_title(self, idx): + def _update_tab_title(self, idx): """Update the tab text for the given tab.""" tab = self.widget(idx) fields = self.get_tab_fields(idx) @@ -189,21 +189,20 @@ class TabWidget(QTabWidget): fields['scroll_pos'] = scroll_pos return fields - def update_tab_titles(self, option='tabs.title.format'): + def _update_tab_titles(self): """Update all texts.""" - if option in ['tabs.title.format', 'tabs.title.format_pinned']: - for idx in range(self.count()): - self.update_tab_title(idx) + for idx in range(self.count()): + self._update_tab_title(idx) def tabInserted(self, idx): """Update titles when a tab was inserted.""" super().tabInserted(idx) - self.update_tab_titles() + self._update_tab_titles() def tabRemoved(self, idx): """Update titles when a tab was removed.""" super().tabRemoved(idx) - self.update_tab_titles() + self._update_tab_titles() def addTab(self, page, icon_or_text, text_or_empty=None): """Override addTab to use our own text setting logic. @@ -304,24 +303,17 @@ class TabBar(QTabBar): super().__init__(parent) self._win_id = win_id self.setStyle(TabBarStyle()) - self.set_font() - config_obj = objreg.get('config') - config_obj.changed.connect(self.set_font) - config_obj.changed.connect(self.set_icon_size) + self._set_font() + config.instance.changed.connect(self._on_config_changed) self.vertical = False self._auto_hide_timer = QTimer() self._auto_hide_timer.setSingleShot(True) - self._auto_hide_timer.setInterval( - config.val.tabs.show_switching_delay) self._auto_hide_timer.timeout.connect(self.maybe_hide) + self._on_show_switching_delay_changed() self.setAutoFillBackground(True) - self.set_colors() + self._set_colors() self.pinned_count = 0 - config_obj.changed.connect(self.set_colors) QTimer.singleShot(0, self.maybe_hide) - config_obj.changed.connect(self.on_tab_colors_changed) - config_obj.changed.connect(self.on_show_switching_delay_changed) - config_obj.changed.connect(self.tabs_show) def __repr__(self): return utils.get_repr(self, count=self.count()) @@ -330,13 +322,23 @@ class TabBar(QTabBar): """Get the current tab object.""" return self.parent().currentWidget() - @config.change_filter('tabs.show') - def tabs_show(self): - """Hide or show tab bar if needed when tabs->show got changed.""" - self.maybe_hide() + @pyqtSlot(str) + def _on_config_changed(self, option): + if option == 'fonts.tabbar': + self._set_font() + elif option == 'tabs.favicons.scale': + self._set_icon_size() + elif option == 'colors.tabs.bar.bg': + self._set_colors() + elif option == 'tabs.show_switching_delay': + self._on_show_switching_delay_changed() + elif option == 'tabs.show': + self.maybe_hide() - @config.change_filter('tabs.show_switching_delay') - def on_show_switching_delay_changed(self): + if option.startswith('colors.tabs.'): + self.update() + + def _on_show_switching_delay_changed(self): """Set timer interval when tabs->show-switching-delay got changed.""" self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay) @@ -405,32 +407,23 @@ class TabBar(QTabBar): # code sets layoutDirty so it actually relayouts the tabs. self.setIconSize(self.iconSize()) - @config.change_filter('fonts.tabbar') - def set_font(self): + def _set_font(self): """Set the tab bar font.""" self.setFont(config.val.fonts.tabbar) - self.set_icon_size() + self._set_icon_size() - @config.change_filter('tabs.favicons.scale') - def set_icon_size(self): + def _set_icon_size(self): """Set the tab bar favicon size.""" size = self.fontMetrics().height() - 2 size *= config.val.tabs.favicons.scale self.setIconSize(QSize(size, size)) - @config.change_filter('colors.tabs.bar.bg') - def set_colors(self): + def _set_colors(self): """Set the tab bar colors.""" p = self.palette() p.setColor(QPalette.Window, config.val.colors.tabs.bar.bg) self.setPalette(p) - @pyqtSlot(str) - def on_tab_colors_changed(self, option): - """Set the tab colors.""" - if option.startswith('colors.tabs.'): - self.update() - def mousePressEvent(self, e): """Override mousePressEvent to close tabs if configured.""" button = config.val.tabs.close_mouse_button diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 8aab14704..b5ca8dd78 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -51,8 +51,8 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): _namespace: The local namespace of the interpreter. """ super().__init__(parent=parent) - self.update_font() - objreg.get('config').changed.connect(self.update_font) + self._update_font() + config.instance.changed.connect(self._update_font) self._history = cmdhistory.History(parent=self) self.returnPressed.connect(self.on_return_pressed) @@ -103,7 +103,7 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): super().keyPressEvent(e) @config.change_filter('fonts.debug_console') - def update_font(self): + def _update_font(self): """Set the correct font.""" self.setFont(config.val.fonts.debug_console) @@ -116,15 +116,15 @@ class ConsoleTextEdit(QTextEdit): super().__init__(parent) self.setAcceptRichText(False) self.setReadOnly(True) - objreg.get('config').changed.connect(self.update_font) - self.update_font() + config.instance.changed.connect(self._update_font) + self._update_font() self.setFocusPolicy(Qt.ClickFocus) def __repr__(self): return utils.get_repr(self) @config.change_filter('fonts.debug_console') - def update_font(self): + def _update_font(self): """Update font when config changed.""" self.setFont(config.val.fonts.debug_console) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 98fa83753..59f2f4a62 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -255,6 +255,7 @@ class _CrashDialog(QDialog): except Exception: self._crash_info.append(("Version info", traceback.format_exc())) try: + # FIXME:conf conf = objreg.get('config') self._crash_info.append(("Config", conf.dump_userconfig())) except Exception: @@ -635,6 +636,7 @@ def dump_exception_info(exc, pages, cmdhist, qobjects): traceback.print_exc() print("\n---- Config ----", file=sys.stderr) try: + # FIXME:conf conf = objreg.get('config') print(conf.dump_userconfig(), file=sys.stderr) except Exception: diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index f137b91f3..97b22508c 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -26,7 +26,7 @@ import contextlib from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject -from qutebrowser.utils import log, utils, objreg, qtutils +from qutebrowser.utils import log, utils, qtutils from qutebrowser.config import config @@ -278,7 +278,7 @@ class LimitLineParser(LineParser): super().__init__(configdir, fname, binary=binary, parent=parent) self._limit = limit if limit is not None and configdir is not None: - objreg.get('config').changed.connect(self.cleanup_file) + config.instance.changed.connect(self._cleanup_file) def __repr__(self): return utils.get_repr(self, constructor=True, @@ -286,7 +286,7 @@ class LimitLineParser(LineParser): limit=self._limit, binary=self._binary) @pyqtSlot(str) - def cleanup_file(self, option): + def _cleanup_file(self, option): """Delete the file if the limit was changed to 0.""" assert self._configfile is not None if option != self._limit: diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index 7b38f907f..8bef9b163 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QObject, QTimer from qutebrowser.config import config from qutebrowser.commands import cmdutils -from qutebrowser.utils import utils, log, message, objreg, usertypes +from qutebrowser.utils import utils, log, message, usertypes class Saveable: @@ -123,11 +123,11 @@ class SaveManager(QObject): We don't do this in __init__ because the config needs to be initialized first, but the config needs the save manager. """ - self.set_autosave_interval() - objreg.get('config').changed.connect(self.set_autosave_interval) + self._set_autosave_interval() + config.instance.changed.connect(self._set_autosave_interval) @config.change_filter('auto_save.interval') - def set_autosave_interval(self): + def _set_autosave_interval(self): """Set the auto-save interval.""" interval = config.val.auto_save.interval if interval == 0: diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 41b44de1f..74c68890e 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -168,6 +168,7 @@ def debug_all_objects(): @cmdutils.register(debug=True) def debug_cache_stats(): """Print LRU cache stats.""" + # FIXME:conf config_info = objreg.get('config').get.cache_info() style_info = style.get_stylesheet.cache_info() log.misc.debug('config: {}'.format(config_info)) From 7ddce62cd642f7f6476c812b8385805de9a06046 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Jun 2017 21:47:26 +0200 Subject: [PATCH 053/516] Refactor most of remaining config.get() calls --- qutebrowser/browser/commands.py | 2 +- qutebrowser/browser/downloads.py | 13 ++++++------- qutebrowser/browser/navigate.py | 4 ++-- qutebrowser/browser/shared.py | 2 +- qutebrowser/config/config.py | 5 ----- qutebrowser/misc/lineparser.py | 4 ++-- qutebrowser/misc/savemanager.py | 2 +- 7 files changed, 13 insertions(+), 19 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3bd5569ee..0b46730f8 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1547,7 +1547,7 @@ class CommandDispatcher: raise cmdexc.CommandError("Invalid help topic {}!".format( topic)) try: - config.get(*parts) + config.instance.get(*parts) except configexc.NoSectionError: raise cmdexc.CommandError("Invalid section {}!".format( parts[0])) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 7b10ef30b..c642a56ab 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -91,8 +91,7 @@ def immediate_download_path(prompt_download_directory=None): storage->prompt-download-directory setting. """ if prompt_download_directory is None: - prompt_download_directory = config.get('storage', - 'prompt-download-directory') + prompt_download_directory = config.val.downloads.location.prompt if not prompt_download_directory: return download_dir() @@ -104,7 +103,7 @@ def _path_suggestion(filename): Args: filename: The filename to use if included in the suggestion. """ - suggestion = config.val.completion.downloads.location.suggestion + suggestion = config.val.downloads.location.suggestion if suggestion == 'path': # add trailing '/' if not present return os.path.join(download_dir(), '') @@ -473,10 +472,10 @@ class AbstractDownloadItem(QObject): position: The color type requested, can be 'fg' or 'bg'. """ assert position in ["fg", "bg"] - start = config.get('colors', 'downloads.{}.start'.format(position)) - stop = config.get('colors', 'downloads.{}.stop'.format(position)) - system = config.get('colors', 'downloads.{}.system'.format(position)) - error = config.get('colors', 'downloads.{}.error'.format(position)) + start = getattr(config.val.colors.downloads.start, position) + stop = getattr(config.val.colors.downloads.stop, position) + system = getattr(config.val.colors.downloads.system, position) + error = getattr(config.val.colors.downloads.error, position) if self.error_msg is not None: assert not self.successful return error diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 92126c6ad..a77853908 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -80,10 +80,10 @@ def _find_prevnext(prev, elems): # Then check for regular links/buttons. elems = [e for e in elems if e.tag_name() != 'link'] - option = 'prev-regexes' if prev else 'next-regexes' + option = 'prev_regexes' if prev else 'next_regexes' if not elems: return None - for regex in config.get('hints', option): + for regex in getattr(config.val.hints, option): log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern)) for e in elems: text = str(e) diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 670c9f4f8..f0a961436 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -180,7 +180,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on): Return: The Question object if a question was asked, None otherwise. """ - config_val = config.get(*option) + config_val = config.instance.get(*option) if config_val == 'ask': if url.isValid(): text = "Allow the website at {} to {}?".format( diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 569bd27d2..8a942632b 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -58,11 +58,6 @@ val = None instance = None -def get(*args, **kwargs): - """Convenience method to call get(...) of the config instance.""" - return instance.get(*args, **kwargs) - - def _init_main_config(parent=None): """Initialize the main config. diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 97b22508c..bd07f7903 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -291,14 +291,14 @@ class LimitLineParser(LineParser): assert self._configfile is not None if option != self._limit: return - value = config.get(option) + value = config.instance.get(option) if value == 0: if os.path.exists(self._configfile): os.remove(self._configfile) def save(self): """Save the config file.""" - limit = config.get(self._limit) + limit = config.instance.get(self._limit) if limit == 0: return do_save = self._prepare_save() diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index 8bef9b163..926bf1230 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -81,7 +81,7 @@ class Saveable: force: Force saving, no matter what. """ if (self._config_opt is not None and - (not config.get(self._config_opt)) and + (not config.instance.get(self._config_opt)) and (not explicit) and (not force)): if not silent: log.save.debug("Not saving {name} because autosaving has been " From 41565fcfd426c4f4d7c4acc868f0083b12d70489 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 11:38:27 +0200 Subject: [PATCH 054/516] configtypes: Use from_py for List/Dict values from a string --- qutebrowser/config/configtypes.py | 65 +++++++------------------------ 1 file changed, 14 insertions(+), 51 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 85fa6a07d..a19bc46f0 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -364,11 +364,6 @@ class List(BaseType): def get_valid_values(self): return self.valtype.get_valid_values() - def _validate_list(self, value): - if self.length is not None and len(value) != self.length: - raise configexc.ValidationError(value, "Exactly {} values need to " - "be set!".format(self.length)) - def from_str(self, value): self._basic_validation(value, pytype=str) if not value: @@ -378,21 +373,19 @@ class List(BaseType): json_val = json.loads(value) except ValueError as e: raise configexc.ValidationError(value, str(e)) - # Can't use self.from_py here because we don't want to call from_py on - # the values. - self._basic_validation(json_val, pytype=list) - if not json_val: - return None - self._validate_list(json_val) - return [self.valtype.from_str(v) for v in json_val] + # For the values, we actually want to call from_py, as we did parse them + # from JSON, so they are numbers/booleans/... already. + return self.from_py(json_val) def from_py(self, value): self._basic_validation(value, pytype=list) if not value: return None - self._validate_list(value) + if self.length is not None and len(value) != self.length: + raise configexc.ValidationError(value, "Exactly {} values need to " + "be set!".format(self.length)) return [self.valtype.from_py(v) for v in value] @@ -417,18 +410,10 @@ class FlagList(List): raise configexc.ValidationError( values, "List contains duplicate values!") - def from_str(self, value): - vals = super().from_str(value) - if vals is not None: - self._check_duplicates(vals) - self._validate_list(vals) - return vals - def from_py(self, value): vals = super().from_py(value) if vals is not None: self._check_duplicates(vals) - self._validate_list(vals) return vals def complete(self): @@ -936,16 +921,9 @@ class Dict(BaseType): except ValueError as e: raise configexc.ValidationError(value, str(e)) - # Can't use self.from_py here because we don't want to call from_py on - # the values. - self._basic_validation(json_val, pytype=dict) - if not json_val: - return None - - self._validate_keys(json_val) - - return {self.keytype.from_str(key): self.valtype.from_str(val) - for key, val in json_val.items()} + # For the values, we actually want to call from_py, as we did parse them + # from JSON, so they are numbers/booleans/... already. + return self.from_py(json_val) def from_py(self, value): self._basic_validation(value, pytype=dict) @@ -1172,12 +1150,6 @@ class Padding(Dict): # FIXME:conf assert valid_values is None, valid_values - def from_str(self, value): - d = super().from_str(value) - if not d: - return None - return PaddingValues(**d) - def from_py(self, value): d = super().from_py(value) if not d: @@ -1310,8 +1282,11 @@ class ConfirmQuit(FlagList): "downloads are running"), ('never', "Never show a confirmation.")) - def _check_values(self, values): - """Check whether the values can be combined in the way they are.""" + def from_py(self, value): + values = super().from_py(value) + if not values: + return None + # Never can't be set with other options if 'never' in values and len(values) > 1: raise configexc.ValidationError( @@ -1321,18 +1296,6 @@ class ConfirmQuit(FlagList): raise configexc.ValidationError( values, "List cannot contain always!") - def from_py(self, value): - values = super().from_py(value) - if not values: - return None - self._check_values(values) - return values - - def from_str(self, value): - values = super().from_str(value) - if not values: - return None - self._check_values(values) return values From 51a29468be74807c3553c84651b59bab1345a950 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 11:40:47 +0200 Subject: [PATCH 055/516] configtypes: Use YAML for loading List/Dict from a string This allows for a more lightweight syntax (like "{a: b}"). --- qutebrowser/config/configtypes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index a19bc46f0..b7f40ebb9 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -20,7 +20,6 @@ """Setting options used for qutebrowser.""" import re -import json import shlex import codecs import os.path @@ -29,6 +28,7 @@ import collections import warnings import datetime +import yaml from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import QTabWidget, QTabBar @@ -370,13 +370,13 @@ class List(BaseType): return None try: - json_val = json.loads(value) - except ValueError as e: + yaml_val = utils.yaml_load(value) + except yaml.YAMLError as e: raise configexc.ValidationError(value, str(e)) # For the values, we actually want to call from_py, as we did parse them - # from JSON, so they are numbers/booleans/... already. - return self.from_py(json_val) + # from YAML, so they are numbers/booleans/... already. + return self.from_py(yaml_val) def from_py(self, value): self._basic_validation(value, pytype=list) @@ -917,13 +917,13 @@ class Dict(BaseType): return None try: - json_val = json.loads(value) - except ValueError as e: + yaml_val = utils.yaml_load(value) + except yaml.YAMLError as e: raise configexc.ValidationError(value, str(e)) # For the values, we actually want to call from_py, as we did parse them - # from JSON, so they are numbers/booleans/... already. - return self.from_py(json_val) + # from YAML, so they are numbers/booleans/... already. + return self.from_py(yaml_val) def from_py(self, value): self._basic_validation(value, pytype=dict) From cdbd64a30d4dd47034be7dac2ed4ab523add24af Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 12:51:20 +0200 Subject: [PATCH 056/516] Move test_configtypes_hypothesis to test_configtypes --- tests/unit/config/test_configtypes.py | 41 ++++++++++++ .../config/test_configtypes_hypothesis.py | 62 ------------------- 2 files changed, 41 insertions(+), 62 deletions(-) delete mode 100644 tests/unit/config/test_configtypes_hypothesis.py diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 61067f0e9..7bcfbe26b 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -19,12 +19,18 @@ """Tests for qutebrowser.config.configtypes.""" import re +import os +import sys import json import collections import itertools import warnings +import inspect +import functools import pytest +import hypothesis +from hypothesis import strategies from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QFont from PyQt5.QtNetwork import QNetworkProxy @@ -175,6 +181,41 @@ class TestValidValues: assert vv.descriptions['bar'] == "bar desc" +class TestAll: + + """Various tests which apply to all available config types.""" + + def gen_classes(): + """Yield all configtypes classes to test. + + Not a method as it's used in decorators. + """ + for _name, member in inspect.getmembers(configtypes, inspect.isclass): + if member in [configtypes.BaseType, configtypes.MappingType, + configtypes._Numeric]: + pass + elif member is configtypes.List: + yield functools.partial(member, valtype=configtypes.Int()) + yield functools.partial(member, valtype=configtypes.Url()) + elif member is configtypes.Dict: + yield functools.partial(member, keytype=configtypes.String(), + valtype=configtypes.String()) + elif member is configtypes.FormatString: + yield functools.partial(member, fields=['a', 'b']) + elif issubclass(member, configtypes.BaseType): + yield member + + @pytest.mark.usefixtures('qapp', 'config_tmpdir') + @pytest.mark.parametrize('klass', gen_classes()) + @hypothesis.given(strategies.text()) + @hypothesis.example('\x00') + def test_from_str_hypothesis(self, klass, s): + try: + klass().from_str(s) + except configexc.ValidationError: + pass + + class TestBaseType: @pytest.fixture diff --git a/tests/unit/config/test_configtypes_hypothesis.py b/tests/unit/config/test_configtypes_hypothesis.py deleted file mode 100644 index 96f47622e..000000000 --- a/tests/unit/config/test_configtypes_hypothesis.py +++ /dev/null @@ -1,62 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-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 . - -"""Hypothesis tests for qutebrowser.config.configtypes.""" - -import os -import sys -import inspect -import functools - -import pytest -import hypothesis -from hypothesis import strategies - -from qutebrowser.config import configtypes, configexc - - -def gen_classes(): - for _name, member in inspect.getmembers(configtypes, inspect.isclass): - if member is configtypes.BaseType: - pass - elif member is configtypes.MappingType: - pass - elif member is configtypes.List: - yield functools.partial(member, inner_type=configtypes.Int()) - yield functools.partial(member, inner_type=configtypes.Url()) - elif member is configtypes.FormatString: - yield functools.partial(member, fields=['a', 'b']) - elif issubclass(member, configtypes.BaseType): - yield member - - -@pytest.mark.usefixtures('qapp', 'config_tmpdir') -@pytest.mark.parametrize('klass', gen_classes()) -@hypothesis.given(strategies.text()) -@hypothesis.example('\x00') -def test_configtypes_hypothesis(klass, s): - if (klass == configtypes.File and sys.platform == 'linux' and - not os.environ.get('DISPLAY', '')): - pytest.skip("No DISPLAY available") - - try: - klass().validate(s) - except configexc.ValidationError: - pass - else: - klass().transform(s) From 61ba92ae180486a72e8537bd885ba07d31c5293a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 13:04:22 +0200 Subject: [PATCH 057/516] configtypes: Separate str/py basic validation This also ensures the behavior for none_ok is consistent. --- qutebrowser/config/configtypes.py | 103 ++++++++++++++------------ tests/unit/config/test_configtypes.py | 19 +++++ 2 files changed, 74 insertions(+), 48 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index b7f40ebb9..35265445f 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -113,20 +113,17 @@ class BaseType: """Get the type's valid values for documentation.""" return self.valid_values - def _basic_validation(self, value, pytype=None): - """Do some basic validation for the value (empty, non-printable chars). - - Also does a Python typecheck on the value (if coming from YAML/Python). + def _basic_py_validation(self, value, pytype): + """Do some basic validation for Python values (emptyness, type). Arguments: value: The value to check. - pytype: If given, a Python type to check the value against. + pytype: A Python type to check the value against. """ if value is None and not self.none_ok: - raise configexc.ValidationError(value, "may not be empty!") + raise configexc.ValidationError(value, "may not be null!") - if (value is not None and pytype is not None and - not isinstance(value, pytype)): + if value is not None and not isinstance(value, pytype): if isinstance(pytype, tuple): expected = ' or '.join(typ.__name__ for typ in pytype) else: @@ -135,12 +132,21 @@ class BaseType: value, "expected a value of type {} but got {}.".format( expected, type(value).__name__)) - if isinstance(value, str): - if not value and not self.none_ok: - raise configexc.ValidationError(value, "may not be empty!") - if any(ord(c) < 32 or ord(c) == 0x7f for c in value): - raise configexc.ValidationError(value, "may not contain " - "unprintable chars!") + if value is not None and isinstance(value, str): + self._basic_str_validation(value) + + def _basic_str_validation(self, value): + """Do some basic validation for string values (empty, non-printable chars). + + Arguments: + value: The value to check. + """ + assert isinstance(value, str) + if not value and not self.none_ok: + raise configexc.ValidationError(value, "may not be empty!") + if any(ord(c) < 32 or ord(c) == 0x7f for c in value): + raise configexc.ValidationError( + value, "may not contain unprintable chars!") def _validate_valid_values(self, value): """Validate value against possible values. @@ -170,6 +176,7 @@ class BaseType: Return: The transformed value. """ + self._basic_str_validation(value) return self.from_py(value) def from_py(self, value): @@ -239,7 +246,7 @@ class MappingType(BaseType): self.valid_values = valid_values def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None self._validate_valid_values(value.lower()) @@ -300,7 +307,7 @@ class String(BaseType): raise configexc.ValidationError(value, msg) def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -365,7 +372,7 @@ class List(BaseType): return self.valtype.get_valid_values() def from_str(self, value): - self._basic_validation(value, pytype=str) + self._basic_str_validation(value) if not value: return None @@ -379,7 +386,7 @@ class List(BaseType): return self.from_py(yaml_val) def from_py(self, value): - self._basic_validation(value, pytype=list) + self._basic_py_validation(value, list) if not value: return None @@ -446,11 +453,11 @@ class Bool(BaseType): self.valid_values = ValidValues('true', 'false') def from_py(self, value): - self._basic_validation(value, pytype=bool) + self._basic_py_validation(value, bool) return value def from_str(self, value): - self._basic_validation(value) + self._basic_str_validation(value) if not value: return None @@ -529,7 +536,7 @@ class Int(_Numeric): """Base class for an integer setting.""" def from_str(self, value): - self._basic_validation(value, pytype=str) + self._basic_str_validation(value) if not value: return None @@ -540,7 +547,7 @@ class Int(_Numeric): return self.from_py(intval) def from_py(self, value): - self._basic_validation(value, pytype=int) + self._basic_py_validation(value, int) self._validate_bounds(value) return value @@ -550,7 +557,7 @@ class Float(_Numeric): """Base class for a float setting.""" def from_str(self, value): - self._basic_validation(value, pytype=str) + self._basic_str_validation(value) if not value: return None @@ -561,7 +568,7 @@ class Float(_Numeric): return self.from_py(floatval) def from_py(self, value): - self._basic_validation(value, pytype=(int, float)) + self._basic_py_validation(value, (int, float)) self._validate_bounds(value) return value @@ -571,7 +578,7 @@ class Perc(_Numeric): """A percentage, as a string ending with %.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -607,7 +614,7 @@ class PercOrInt(_Numeric): "({})!".format(self.minperc, self.maxperc)) def from_str(self, value): - self._basic_validation(value) + self._basic_str_validation(value) if not value: return None @@ -623,7 +630,7 @@ class PercOrInt(_Numeric): def from_py(self, value): """Expect a value like '42%' as string, or 23 as int.""" - self._basic_validation(value, pytype=(int, str)) + self._basic_py_validation(value, (int, str)) if not value: return @@ -656,7 +663,7 @@ class Command(BaseType): def from_py(self, value): # FIXME:conf require a list here? - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return split = value.split() @@ -697,7 +704,7 @@ class QtColor(BaseType): """Base class for QColor.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -713,7 +720,7 @@ class QssColor(BaseType): """Color used in a Qt stylesheet.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -751,7 +758,7 @@ class Font(BaseType): (?P.+)$ # mandatory font family""", re.VERBOSE) def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -768,7 +775,7 @@ class FontFamily(Font): """A Qt font family.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -788,7 +795,7 @@ class QtFont(Font): """A Font which gets converted to a QFont.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -885,7 +892,7 @@ class Regex(BaseType): def from_py(self, value): """Get a compiled regex from either a string or a regex object.""" - self._basic_validation(value, pytype=(str, self._regex_type)) + self._basic_py_validation(value, (str, self._regex_type)) if not value: return None elif isinstance(value, str): @@ -912,7 +919,7 @@ class Dict(BaseType): value, "Expected keys {}".format(self.fixed_keys)) def from_str(self, value): - self._basic_validation(value, pytype=str) + self._basic_str_validation(value) if not value: return None @@ -926,7 +933,7 @@ class Dict(BaseType): return self.from_py(yaml_val) def from_py(self, value): - self._basic_validation(value, pytype=dict) + self._basic_py_validation(value, dict) if not value: return None @@ -945,7 +952,7 @@ class File(BaseType): self.required = required def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -971,7 +978,7 @@ class Directory(BaseType): """A directory on the local filesystem.""" def from_py(self, value): - self._basic_validation(value) + self._basic_py_validation(value, dict) if not value: return value = os.path.expandvars(value) @@ -998,7 +1005,7 @@ class FormatString(BaseType): self.fields = fields def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -1026,7 +1033,7 @@ class ShellCommand(BaseType): self.placeholder = placeholder def from_str(self, value): - self._basic_validation(value, pytype=str) + self._basic_str_validation(value) if not value: return None try: @@ -1036,7 +1043,7 @@ class ShellCommand(BaseType): def from_py(self, value): # FIXME:conf require a str/list here? - self._basic_validation(value, pytype=list) + self._basic_py_validation(value, list) if not value: return None @@ -1058,7 +1065,7 @@ class Proxy(BaseType): def from_py(self, value): from qutebrowser.utils import urlutils - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -1094,7 +1101,7 @@ class SearchEngineUrl(BaseType): """A search engine URL.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -1123,7 +1130,7 @@ class FuzzyUrl(BaseType): def from_py(self, value): from qutebrowser.utils import urlutils - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -1162,7 +1169,7 @@ class Encoding(BaseType): """Setting for a python encoding.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None try: @@ -1219,7 +1226,7 @@ class Url(BaseType): """A URL.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None @@ -1235,7 +1242,7 @@ class SessionName(BaseType): """The name of a session.""" def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None if value.startswith('_'): @@ -1322,7 +1329,7 @@ class TimestampTemplate(BaseType): """ def from_py(self, value): - self._basic_validation(value, pytype=str) + self._basic_py_validation(value, str) if not value: return None diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 7bcfbe26b..945862041 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -215,6 +215,25 @@ class TestAll: except configexc.ValidationError: pass + @pytest.mark.parametrize('klass', gen_classes()) + def test_none_ok_true(self, klass): + """Test None and empty string values with none_ok=True.""" + typ = klass(none_ok=True) + assert typ.from_str('') is None + assert typ.from_py(None) is None + + @pytest.mark.parametrize('method, value', [ + ('from_str', ''), + ('from_py', ''), + ('from_py', None) + ]) + @pytest.mark.parametrize('klass', gen_classes()) + def test_none_ok_false(self, klass, method, value): + """Test None and empty string values with none_ok=False.""" + meth = getattr(klass(), method) + with pytest.raises(configexc.ValidationError): + meth(value) + class TestBaseType: From ffd1a9146798028d3cde1a64b0b9ef66559c6be7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 13:04:50 +0200 Subject: [PATCH 058/516] Fix Directory conftype --- qutebrowser/config/configtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 35265445f..c39fdceb7 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -978,9 +978,9 @@ class Directory(BaseType): """A directory on the local filesystem.""" def from_py(self, value): - self._basic_py_validation(value, dict) + self._basic_py_validation(value, str) if not value: - return + return None value = os.path.expandvars(value) value = os.path.expanduser(value) try: From 71f2e8c577a47997826bdbe855b811fd34cf3056 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 13:21:33 +0200 Subject: [PATCH 059/516] None validation fixups for test_configtypes --- tests/unit/config/test_configtypes.py | 226 ++++++++------------------ 1 file changed, 69 insertions(+), 157 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 945862041..a44556a72 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -253,27 +253,30 @@ class TestBaseType: with pytest.raises(configexc.ValidationError): basetype._validate_valid_values('baz') - @pytest.mark.parametrize('val', [None, '', 'foobar', 'snowman: ☃', - 'foo bar']) - def test_basic_validation_valid(self, klass, val): + @pytest.mark.parametrize('val', ['', 'foobar', 'snowman: ☃', 'foo bar']) + def test_basic_str_validation_valid(self, klass, val): """Test _basic_validation with valid values.""" basetype = klass() basetype.none_ok = True - basetype._basic_validation(val) + basetype._basic_str_validation(val) - @pytest.mark.parametrize('val', [None, '', '\x00']) + @pytest.mark.parametrize('val', ['', '\x00']) def test_basic_validation_invalid(self, klass, val): """Test _basic_validation with invalid values.""" with pytest.raises(configexc.ValidationError): - klass()._basic_validation(val) + klass()._basic_str_validation(val) - def test_basic_validation_pytype_valid(self, klass): - klass()._basic_validation([], pytype=list) + def test_basic_py_validation_valid(self, klass): + klass()._basic_py_validation([], list) - def test_basic_validation_pytype_invalid(self, klass): + def test_basic_py_validation_invalid(self, klass): with pytest.raises(configexc.ValidationError, match='expected a value of type str but got list'): - klass()._basic_validation([], pytype=str) + klass()._basic_py_validation([], str) + + def test_basic_py_validation_invalid_str(self, klass): + with pytest.raises(configexc.ValidationError): + klass()._basic_py_validation('\x00', str) def test_complete_none(self, klass): """Test complete with valid_values not set.""" @@ -322,8 +325,6 @@ class MappingSubclass(configtypes.MappingType): class TestMappingType: TESTS = { - None: None, - '': None, 'one': 1, 'two': 2, 'ONE': 1, @@ -335,9 +336,9 @@ class TestMappingType: @pytest.mark.parametrize('val, expected', list(TESTS.items())) def test_from_py(self, klass, val, expected): - assert klass(none_ok=True).from_py(val) == expected + assert klass().from_py(val) == expected - @pytest.mark.parametrize('val', [None, 'one!', 'blah']) + @pytest.mark.parametrize('val', ['one!', 'blah']) def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().from_py(val) @@ -369,8 +370,6 @@ class TestString: klass(minlen=minlen, maxlen=maxlen) @pytest.mark.parametrize('kwargs, val', [ - ({'none_ok': True}, ''), # Empty with none_ok - ({'none_ok': True}, None), # None with none_ok ({}, "Test! :-)"), # Forbidden chars ({'forbidden': 'xyz'}, 'fobar'), @@ -383,11 +382,9 @@ class TestString: ({'valid_values': configtypes.ValidValues('abcd')}, 'abcd'), ]) def test_from_py(self, klass, kwargs, val): - expected = None if not val else val - assert klass(**kwargs).from_py(val) == expected + assert klass(**kwargs).from_py(val) == val @pytest.mark.parametrize('kwargs, val', [ - ({}, ''), # Empty without none_ok # Forbidden chars ({'forbidden': 'xyz'}, 'foybar'), ({'forbidden': 'xyz'}, 'foxbar'), @@ -474,10 +471,7 @@ class TestList: json_val = json.dumps(val) assert klass().from_str(json_val) == val - def test_from_str_empty(self, klass): - assert klass(none_ok_outer=True).from_str('') is None - - @pytest.mark.parametrize('val', ['', '[[', 'true']) + @pytest.mark.parametrize('val', ['[[', 'true']) def test_from_str_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().from_str(val) @@ -577,11 +571,9 @@ class TestBool: 'false': False, 'FaLsE': False, 'off': False, - - '': None, } - INVALID = ['10', 'yess', 'false_', ''] + INVALID = ['10', 'yess', 'false_'] @pytest.fixture def klass(self): @@ -589,21 +581,20 @@ class TestBool: @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) def test_from_str_valid(self, klass, val, expected): - assert klass(none_ok=True).from_str(val) == expected + assert klass().from_str(val) == expected @pytest.mark.parametrize('val', INVALID) def test_from_str_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().from_str(val) - @pytest.mark.parametrize('val', [True, False, None]) + @pytest.mark.parametrize('val', [True, False]) def test_from_py_valid(self, klass, val): - assert klass(none_ok=True).from_py(val) is val + assert klass().from_py(val) is val - @pytest.mark.parametrize('val', [None, 42]) - def test_from_py_invalid(self, klass, val): + def test_from_py_invalid(self, klass): with pytest.raises(configexc.ValidationError): - klass().from_py(val) + klass().from_py(42) class TestBoolAsk: @@ -622,21 +613,20 @@ class TestBoolAsk: @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) def test_from_str_valid(self, klass, val, expected): - assert klass(none_ok=True).from_str(val) == expected + assert klass().from_str(val) == expected @pytest.mark.parametrize('val', INVALID) def test_from_str_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().from_str(val) - @pytest.mark.parametrize('val', [True, False, None, 'ask']) + @pytest.mark.parametrize('val', [True, False, 'ask']) def test_from_py_valid(self, klass, val): - assert klass(none_ok=True).from_py(val) == val + assert klass().from_py(val) == val - @pytest.mark.parametrize('val', [None, 42]) - def test_from_py_invalid(self, klass, val): + def test_from_py_invalid(self, klass): with pytest.raises(configexc.ValidationError): - klass().from_py(val) + klass().from_py(42) class TestNumeric: @@ -694,14 +684,12 @@ class TestInt: @pytest.mark.parametrize('kwargs, val, expected', [ ({}, '1337', 1337), ({}, '0', 0), - ({'none_ok': True}, '', None), ({'minval': 2}, '2', 2), ]) def test_from_str_valid(self, klass, kwargs, val, expected): assert klass(**kwargs).from_str(val) == expected @pytest.mark.parametrize('kwargs, val', [ - ({}, ''), ({}, '2.5'), ({}, 'foobar'), ({'minval': 2, 'maxval': 3}, '1'), @@ -713,14 +701,12 @@ class TestInt: @pytest.mark.parametrize('kwargs, val', [ ({}, 1337), ({}, 0), - ({'none_ok': True}, None), ({'minval': 2}, 2), ]) def test_from_py_valid(self, klass, kwargs, val): assert klass(**kwargs).from_py(val) == val @pytest.mark.parametrize('kwargs, val', [ - ({}, ''), ({}, 2.5), ({}, 'foobar'), ({'minval': 2, 'maxval': 3}, 1), @@ -739,14 +725,12 @@ class TestFloat: @pytest.mark.parametrize('kwargs, val, expected', [ ({}, '1337', 1337), ({}, '1337.42', 1337.42), - ({'none_ok': True}, '', None), ({'minval': 2.00}, '2.00', 2.00), ]) def test_from_str_valid(self, klass, kwargs, val, expected): assert klass(**kwargs).from_str(val) == expected @pytest.mark.parametrize('kwargs, val', [ - ({}, ''), ({}, 'foobar'), ({'minval': 2, 'maxval': 3}, '3.01'), ]) @@ -758,14 +742,12 @@ class TestFloat: ({}, 1337), ({}, 0), ({}, 1337.42), - ({'none_ok': True}, None), ({'minval': 2}, 2.01), ]) def test_from_py_valid(self, klass, kwargs, val): assert klass(**kwargs).from_py(val) == val @pytest.mark.parametrize('kwargs, val', [ - ({}, ''), ({}, 'foobar'), ({'minval': 2, 'maxval': 3}, 1.99), ]) @@ -783,7 +765,6 @@ class TestPerc: @pytest.mark.parametrize('kwargs, val, expected', [ ({}, '1337%', 1337), ({}, '1337.42%', 1337.42), - ({'none_ok': True}, '', None), ({'maxval': 2}, '2%', 2), ]) def test_from_str_valid(self, klass, kwargs, val, expected): @@ -794,7 +775,6 @@ class TestPerc: ({}, '1337%%'), ({}, 'foobar'), ({}, 'foobar%'), - ({}, ''), ({'minval': 2}, '1%'), ({'maxval': 2}, '3%'), ({'minval': 2, 'maxval': 3}, '1%'), @@ -806,14 +786,12 @@ class TestPerc: @pytest.mark.parametrize('kwargs, val, expected', [ ({}, '1337.42%', 1337.42), - ({'none_ok': True}, None, None), ({'minval': 2}, '2.01%', 2.01), ]) def test_from_py_valid(self, klass, kwargs, val, expected): assert klass(**kwargs).from_py(val) == expected @pytest.mark.parametrize('kwargs, val', [ - ({}, ''), ({}, 'foobar'), ({}, 23), ({'minval': 2, 'maxval': 3}, '1.99%'), @@ -842,7 +820,6 @@ class TestPercOrInt: @pytest.mark.parametrize('kwargs, val, expected', [ ({}, '1337%', '1337%'), ({}, '1337', 1337), - ({'none_ok': True}, '', None), ({'minperc': 2}, '2%', '2%'), ({'maxperc': 2}, '2%', '2%'), @@ -861,7 +838,6 @@ class TestPercOrInt: ({}, '1337%%'), ({}, '1337.42%'), ({}, 'foobar'), - ({}, ''), ({'minperc': 2}, '1%'), ({'maxperc': 2}, '3%'), @@ -877,9 +853,9 @@ class TestPercOrInt: with pytest.raises(configexc.ValidationError): klass(**kwargs).from_str(val) - @pytest.mark.parametrize('val', ['1337%', 1337, None]) + @pytest.mark.parametrize('val', ['1337%', 1337]) def test_from_py_valid(self, klass, val): - assert klass(none_ok=True).from_py(val) == val + assert klass().from_py(val) == val @pytest.mark.parametrize('val', ['1337%%', '1337']) def test_from_py_invalid(self, klass, val): @@ -901,13 +877,13 @@ class TestCommand: def klass(self): return configtypes.Command - @pytest.mark.parametrize('val', ['', 'cmd1', 'cmd2', 'cmd1 foo bar', + @pytest.mark.parametrize('val', ['cmd1', 'cmd2', 'cmd1 foo bar', 'cmd2 baz fish']) def test_from_py_valid(self, klass, val): expected = None if not val else val - assert klass(none_ok=True).from_py(val) == expected + assert klass().from_py(val) == expected - @pytest.mark.parametrize('val', ['', 'cmd3', 'cmd3 foo bar', ' ']) + @pytest.mark.parametrize('val', ['cmd3', 'cmd3 foo bar', ' ']) def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().from_py(val) @@ -932,7 +908,6 @@ class ColorTests: ('#111222333', TYPES), ('#111122223333', TYPES), ('red', TYPES), - (None, TYPES), ('#00000G', []), ('#123456789ABCD', []), @@ -995,23 +970,14 @@ class TestColors: @pytest.mark.parametrize('klass, val', TESTS.valid) def test_from_py_valid(self, klass, val): - if not val: - expected = None - elif klass is configtypes.QtColor: - expected = QColor(val) - else: - expected = val - assert klass(none_ok=True).from_py(val) == expected + expected = QColor(val) if klass is configtypes.QtColor else val + assert klass().from_py(val) == expected @pytest.mark.parametrize('klass, val', TESTS.invalid) def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().from_py(val) - def test_validate_invalid_empty(self, klass_fixt): - with pytest.raises(configexc.ValidationError): - klass_fixt().from_py('') - FontDesc = collections.namedtuple('FontDesc', ['style', 'weight', 'pt', 'px', 'family']) @@ -1080,15 +1046,13 @@ class TestFont: def qtfont_class(self): return configtypes.QtFont - @pytest.mark.parametrize('val, desc', sorted(TESTS.items()) + [(None, None)]) + @pytest.mark.parametrize('val, desc', sorted(TESTS.items())) def test_from_py_valid(self, klass, val, desc): - if desc is None: - expected = None - elif klass is configtypes.Font: + if klass is configtypes.Font: expected = val elif klass is configtypes.QtFont: expected = Font.fromdesc(desc) - assert klass(none_ok=True).from_py(val) == expected + assert klass().from_py(val) == expected def test_qtfont_float(self, qtfont_class): """Test QtFont's transform with a float as point size. @@ -1114,8 +1078,6 @@ class TestFont: pytest.param('green', marks=font_xfail), pytest.param('10pt', marks=font_xfail), pytest.param('10pt ""', marks=font_xfail), - '', - None, ]) def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): @@ -1124,7 +1086,7 @@ class TestFont: class TestFontFamily: - TESTS = ['"Foobar Neue"', 'inconsolatazi4', 'Foobar', None] + TESTS = ['"Foobar Neue"', 'inconsolatazi4', 'Foobar'] INVALID = [ '10pt "Foobar Neue"', '10PT "Foobar Neue"', @@ -1140,7 +1102,6 @@ class TestFontFamily: 'oblique 10pt "Foobar Neue"', 'normal bold 10pt "Foobar Neue"', 'bold italic 10pt "Foobar Neue"', - None, # with none_ok=False ] @pytest.fixture @@ -1149,7 +1110,7 @@ class TestFontFamily: @pytest.mark.parametrize('val', TESTS) def test_from_py_valid(self, klass, val): - assert klass(none_ok=True).from_py(val) == val + assert klass().from_py(val) == val @pytest.mark.parametrize('val', INVALID) def test_from_py_invalid(self, klass, val): @@ -1166,15 +1127,12 @@ class TestRegex: @pytest.mark.parametrize('val', [ r'(foo|bar)?baz[fis]h', re.compile('foobar'), - None ]) def test_from_py_valid(self, klass, val): - expected = None if val is None else RegexEq(val) - assert klass(none_ok=True).from_py(val) == expected + assert klass().from_py(val) == RegexEq(val) @pytest.mark.parametrize('val', [ pytest.param(r'(foo|bar))?baz[fis]h', id='unmatched parens'), - pytest.param('', id='empty'), pytest.param('(' * 500, id='too many parens'), ]) def test_from_py_invalid(self, klass, val): @@ -1239,8 +1197,7 @@ class TestDict: @pytest.mark.parametrize('val', [ {"foo": "bar"}, {"foo": "bar", "baz": "fish"}, - '', # empty value with none_ok=true - {}, # ditto + {}, # with none_ok=True ]) def test_from_str_valid(self, klass, val): expected = None if not val else val @@ -1253,7 +1210,6 @@ class TestDict: @pytest.mark.parametrize('val', [ '["foo"]', # valid json but not a dict '{"hello": 23}', # non-string as value - '', # empty value with none_ok=False '[invalid', # invalid json ]) def test_from_str_invalid(self, klass, val): @@ -1293,13 +1249,6 @@ class TestFile: def file_class(self): return configtypes.File - def test_from_py_empty(self, klass): - with pytest.raises(configexc.ValidationError): - klass().from_py('') - - def test_from_py_empty_none_ok(self, klass): - assert klass(none_ok=True).from_py('') is None - def test_from_py_does_not_exist_file(self, os_mock): """Test from_py with a file which does not exist (File).""" os_mock.path.isfile.return_value = False @@ -1315,12 +1264,11 @@ class TestFile: ('/foobar', '/foobar'), ('~/foobar', '/home/foo/foobar'), ('$HOME/foobar', '/home/foo/foobar'), - ('', None), ]) def test_from_py_exists_abs(self, klass, os_mock, val, expected): """Test from_py with a file which does exist.""" os_mock.path.isfile.return_value = True - assert klass(none_ok=True).from_py(val) == expected + assert klass().from_py(val) == expected def test_from_py_exists_rel(self, klass, os_mock, monkeypatch): """Test from_py with a relative path to an existing file.""" @@ -1362,16 +1310,6 @@ class TestDirectory: def klass(self): return configtypes.Directory - def test_from_py_empty(self, klass): - """Test from_py with empty string and none_ok = False.""" - with pytest.raises(configexc.ValidationError): - klass().from_py('') - - def test_from_py_empty_none_ok(self, klass): - """Test from_py with empty string and none_ok = True.""" - t = configtypes.Directory(none_ok=True) - assert t.from_py(None) is None - def test_from_py_does_not_exist(self, klass, os_mock): """Test from_py with a directory which does not exist.""" os_mock.path.isdir.return_value = False @@ -1425,17 +1363,14 @@ class TestFormatString: @pytest.mark.parametrize('val', [ 'foo bar baz', '{foo} {bar} baz', - None, ]) def test_from_py_valid(self, typ, val): - typ.none_ok = True assert typ.from_py(val) == val @pytest.mark.parametrize('val', [ '{foo} {bar} {baz}', '{foo} {bar', '{1}', - None, ]) def test_from_py_invalid(self, typ, val): with pytest.raises(configexc.ValidationError): @@ -1449,7 +1384,6 @@ class TestShellCommand: return configtypes.ShellCommand @pytest.mark.parametrize('kwargs, val, expected', [ - ({'none_ok': True}, '', None), ({}, 'foobar', ['foobar']), ({'placeholder': '{}'}, 'foo {} bar', ['foo', '{}', 'bar']), ({'placeholder': '{}'}, 'foo{}bar', ['foo{}bar']), @@ -1461,7 +1395,6 @@ class TestShellCommand: assert cmd.from_py(expected) == expected @pytest.mark.parametrize('kwargs, val', [ - ({}, ''), ({'placeholder': '{}'}, 'foo bar'), ({'placeholder': '{}'}, 'foo { } bar'), ({}, 'foo"'), # not splittable with shlex @@ -1478,7 +1411,6 @@ class TestProxy: return configtypes.Proxy @pytest.mark.parametrize('val, expected', [ - (None, None), ('system', configtypes.SYSTEM_PROXY), ('none', QNetworkProxy(QNetworkProxy.NoProxy)), ('socks://example.com/', @@ -1492,13 +1424,12 @@ class TestProxy: pac.PACFetcher(QUrl('pac+file:///tmp/proxy.pac'))), ]) def test_from_py_valid(self, klass, val, expected): - actual = klass(none_ok=True).from_py(val) + actual = klass().from_py(val) if isinstance(actual, QNetworkProxy): actual = QNetworkProxy(actual) assert actual == expected @pytest.mark.parametrize('val', [ - '', 'blah', ':', # invalid URL 'ftp://example.com/', # invalid scheme @@ -1526,13 +1457,11 @@ class TestSearchEngineUrl: 'http://example.com/?q={}', 'http://example.com/?q={0}', 'http://example.com/?q={0}&a={0}', - None, # empty value with none_ok ]) def test_from_py_valid(self, klass, val): - assert klass(none_ok=True).from_py(val) == val + assert klass().from_py(val) == val @pytest.mark.parametrize('val', [ - '', # empty value without none_ok 'foo', # no placeholder ':{}', # invalid URL 'foo{bar}baz{}', # {bar} format string variable @@ -1551,15 +1480,13 @@ class TestFuzzyUrl: return configtypes.FuzzyUrl @pytest.mark.parametrize('val, expected', [ - (None, None), ('http://example.com/?q={}', QUrl('http://example.com/?q={}')), ('example.com', QUrl('http://example.com')), ]) def test_from_py_valid(self, klass, val, expected): - assert klass(none_ok=True).from_py(val) == expected + assert klass().from_py(val) == expected @pytest.mark.parametrize('val', [ - None, '::foo', # invalid URL 'foo bar', # invalid search term ]) @@ -1574,24 +1501,17 @@ class TestPadding: def klass(self): return configtypes.Padding - @pytest.mark.parametrize('val, expected', [ - (None, None), - ({'top': 1, 'bottom': 2, 'left': 3, 'right': 4}, - configtypes.PaddingValues(1, 2, 3, 4)), - ]) - def test_from_py_valid(self, klass, val, expected): - assert klass(none_ok=True).from_py(val) == expected + def test_from_py_valid(self, klass): + val = {'top': 1, 'bottom': 2, 'left': 3, 'right': 4} + expected = configtypes.PaddingValues(1, 2, 3, 4) + assert klass().from_py(val) == expected - @pytest.mark.parametrize('val, expected', [ - ('', None), - ('{"top": 1, "bottom": 2, "left": 3, "right": 4}', - configtypes.PaddingValues(1, 2, 3, 4)), - ]) - def test_from_str_valid(self, klass, val, expected): - assert klass(none_ok=True).from_str(val) == expected + def test_from_str_valid(self, klass): + val = '{"top": 1, "bottom": 2, "left": 3, "right": 4}' + expected = configtypes.PaddingValues(1, 2, 3, 4) + assert klass().from_str(val) == expected @pytest.mark.parametrize('val', [ - None, {'top': 1, 'bottom': 2, 'left': 3, 'right': 4, 'foo': 5}, {'top': 1, 'bottom': 2, 'left': 3, 'right': 'four'}, {'top': 1, 'bottom': 2}, @@ -1609,14 +1529,13 @@ class TestEncoding: def klass(self): return configtypes.Encoding - @pytest.mark.parametrize('val', ['utf-8', 'UTF-8', 'iso8859-1', None]) + @pytest.mark.parametrize('val', ['utf-8', 'UTF-8', 'iso8859-1']) def test_from_py(self, klass, val): - assert klass(none_ok=True).from_py(val) == val + assert klass().from_py(val) == val - @pytest.mark.parametrize('val', ['blubber', '']) - def test_validate_invalid(self, klass, val): + def test_validate_invalid(self, klass): with pytest.raises(configexc.ValidationError): - klass().from_py(val) + klass().from_py('blubber') class TestUrl: @@ -1624,7 +1543,6 @@ class TestUrl: TESTS = { 'http://qutebrowser.org/': QUrl('http://qutebrowser.org/'), 'http://heise.de/': QUrl('http://heise.de/'), - None: None, } @pytest.fixture @@ -1633,12 +1551,11 @@ class TestUrl: @pytest.mark.parametrize('val, expected', list(TESTS.items())) def test_from_py_valid(self, klass, val, expected): - assert klass(none_ok=True).from_py(val) == expected + assert klass().from_py(val) == expected - @pytest.mark.parametrize('val', [None, '+']) - def test_from_py_invalid(self, klass, val): + def test_from_py_invalid(self, klass): with pytest.raises(configexc.ValidationError): - klass().from_py(val) + klass().from_py('+') class TestSessionName: @@ -1647,20 +1564,17 @@ class TestSessionName: def klass(self): return configtypes.SessionName - @pytest.mark.parametrize('val', [None, 'foobar']) - def test_from_py_valid(self, klass, val): - assert klass(none_ok=True).from_py(val) == val + def test_from_py_valid(self, klass): + assert klass().from_py('foobar') == 'foobar' - @pytest.mark.parametrize('val', [None, '_foo']) - def test_from_py_invalid(self, klass, val): + def test_from_py_invalid(self, klass): with pytest.raises(configexc.ValidationError): - klass().from_py(val) + klass().from_py('_foo') class TestConfirmQuit: TESTS = [ - None, ['multiple-tabs', 'downloads'], ['downloads', 'multiple-tabs'], ['downloads', None, 'multiple-tabs'], @@ -1677,7 +1591,6 @@ class TestConfirmQuit: assert cq.from_str(json.dumps(val)) == val @pytest.mark.parametrize('val', [ - None, # with none_ok=False ['foo'], ['downloads', 'foo'], # valid value mixed with invalid one ['downloads', 'multiple-tabs', 'downloads'], # duplicate value @@ -1707,14 +1620,13 @@ class TestTimestampTemplate: def klass(self): return configtypes.TimestampTemplate - @pytest.mark.parametrize('val', [None, 'foobar', '%H:%M', 'foo %H bar %M']) + @pytest.mark.parametrize('val', ['foobar', '%H:%M', 'foo %H bar %M']) def test_from_py_valid(self, klass, val): - assert klass(none_ok=True).from_py(val) == val + assert klass().from_py(val) == val - @pytest.mark.parametrize('val', [None, '%']) - def test_from_py_invalid(self, klass, val): + def test_from_py_invalid(self, klass): with pytest.raises(configexc.ValidationError): - klass().from_py(val) + klass().from_py('%') @pytest.mark.parametrize('first, second, equal', [ From 7416164aca89b09bcee9d27442c4763d9245e232 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 13:23:13 +0200 Subject: [PATCH 060/516] Rename old validate tests --- tests/unit/config/test_configtypes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index a44556a72..b5637b77f 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -492,7 +492,7 @@ class TestList: klass(none_ok_inner=True).from_py(None) @pytest.mark.parametrize('val', [None, ['foo', 'bar']]) - def test_validate_length(self, klass, val): + def test_from_py_length(self, klass, val): klass(none_ok_outer=True, length=2).from_py(val) @pytest.mark.parametrize('val', [['a'], ['a', 'b'], ['a', 'b', 'c', 'd']]) @@ -661,7 +661,7 @@ class TestNumeric: ({'minval': 2, 'maxval': 3}, 1, False), ({'minval': 2, 'maxval': 3}, 4, False), ]) - def test_validate_invalid(self, klass, kwargs, val, valid): + def test_validate_bounds_invalid(self, klass, kwargs, val, valid): if valid: klass(**kwargs)._validate_bounds(val) else: @@ -1055,7 +1055,7 @@ class TestFont: assert klass().from_py(val) == expected def test_qtfont_float(self, qtfont_class): - """Test QtFont's transform with a float as point size. + """Test QtFont's from_py with a float as point size. We can't test the point size for equality as Qt seems to do some rounding as appropriate. @@ -1518,7 +1518,7 @@ class TestPadding: {'top': -1, 'bottom': 2, 'left': 3, 'right': 4}, {'top': 0.1, 'bottom': 2, 'left': 3, 'right': 4}, ]) - def test_validate_invalid(self, klass, val): + def test_from_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().from_py(val) @@ -1533,7 +1533,7 @@ class TestEncoding: def test_from_py(self, klass, val): assert klass().from_py(val) == val - def test_validate_invalid(self, klass): + def test_from_py_invalid(self, klass): with pytest.raises(configexc.ValidationError): klass().from_py('blubber') From 05dc94ccc406725988a46a2653fad75dcc6524dc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 13:54:48 +0200 Subject: [PATCH 061/516] Improve configtypes tests --- tests/unit/config/test_configtypes.py | 112 ++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 16 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index b5637b77f..4f20cb818 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -21,7 +21,6 @@ import re import os import sys -import json import collections import itertools import warnings @@ -205,8 +204,11 @@ class TestAll: elif issubclass(member, configtypes.BaseType): yield member + @pytest.fixture(params=list(gen_classes())) + def klass(self, request): + return request.param + @pytest.mark.usefixtures('qapp', 'config_tmpdir') - @pytest.mark.parametrize('klass', gen_classes()) @hypothesis.given(strategies.text()) @hypothesis.example('\x00') def test_from_str_hypothesis(self, klass, s): @@ -215,7 +217,6 @@ class TestAll: except configexc.ValidationError: pass - @pytest.mark.parametrize('klass', gen_classes()) def test_none_ok_true(self, klass): """Test None and empty string values with none_ok=True.""" typ = klass(none_ok=True) @@ -227,13 +228,17 @@ class TestAll: ('from_py', ''), ('from_py', None) ]) - @pytest.mark.parametrize('klass', gen_classes()) def test_none_ok_false(self, klass, method, value): """Test None and empty string values with none_ok=False.""" meth = getattr(klass(), method) with pytest.raises(configexc.ValidationError): meth(value) + def test_invalid_python_type(self, klass): + """Make sure every type fails when passing an invalid Python type.""" + with pytest.raises(configexc.ValidationError): + klass().from_py(object()) + class TestBaseType: @@ -432,9 +437,11 @@ class ListSubclass(configtypes.List): Valid values are 'foo', 'bar' and 'baz'. """ - def __init__(self, none_ok_inner=False, none_ok_outer=False, length=None): - super().__init__(configtypes.String(none_ok=none_ok_inner), - none_ok=none_ok_outer, length=length) + def __init__(self, none_ok_inner=False, none_ok_outer=False, length=None, + elemtype=None): + if elemtype is None: + elemtype = configtypes.String(none_ok=none_ok_inner) + super().__init__(elemtype, none_ok=none_ok_outer, length=length) self.valtype.valid_values = configtypes.ValidValues( 'foo', 'bar', 'baz') @@ -459,7 +466,6 @@ class TestList: """Test List and FlagList.""" - # FIXME:conf make sure from_str/from_py is called on valtype. # FIXME:conf how to handle []? @pytest.fixture(params=[ListSubclass, FlagListSubclass]) @@ -468,8 +474,12 @@ class TestList: @pytest.mark.parametrize('val', [['foo'], ['foo', 'bar']]) def test_from_str(self, klass, val): - json_val = json.dumps(val) - assert klass().from_str(json_val) == val + yaml_val = utils.yaml_dump(val) + assert klass().from_str(yaml_val) == val + + def test_from_str_int(self): + typ = configtypes.List(valtype=configtypes.Int()) + assert typ.from_str(utils.yaml_dump([0])) == [0] @pytest.mark.parametrize('val', ['[[', 'true']) def test_from_str_invalid(self, klass, val): @@ -512,6 +522,21 @@ class TestList: expected = configtypes.ValidValues('foo', 'bar', 'baz') assert klass().get_valid_values() == expected + @hypothesis.given(val=strategies.lists(strategies.just('foo'))) + def test_hypothesis(self, klass, val): + try: + klass().from_py(val) + except configexc.ValidationError: + pass + + @hypothesis.given(val=strategies.lists(strategies.just('foo'))) + def test_hypothesis_text(self, klass, val): + text = utils.yaml_dump(val).decode('utf-8') + try: + klass().from_str(text) + except configexc.ValidationError: + pass + class TestFlagList: @@ -715,6 +740,18 @@ class TestInt: with pytest.raises(configexc.ValidationError): klass(**kwargs).from_py(val) + @hypothesis.given(val=strategies.integers()) + def test_hypothesis(self, klass, val): + klass().from_py(val) + + @hypothesis.given(val=strategies.integers()) + def test_hypothesis_text(self, klass, val): + text = utils.yaml_dump(val).decode('utf-8') + try: + klass().from_str(text) + except configexc.ValidationError: + pass + class TestFloat: @@ -755,6 +792,20 @@ class TestFloat: with pytest.raises(configexc.ValidationError): klass(**kwargs).from_py(val) + @hypothesis.given(val=strategies.one_of(strategies.floats(), + strategies.integers())) + def test_hypothesis(self, klass, val): + klass().from_py(val) + + @hypothesis.given(val=strategies.one_of(strategies.floats(), + strategies.integers())) + def test_hypothesis_text(self, klass, val): + text = utils.yaml_dump(val).decode('utf-8') + try: + klass().from_str(text) + except configexc.ValidationError: + pass + class TestPerc: @@ -862,6 +913,14 @@ class TestPercOrInt: with pytest.raises(configexc.ValidationError): klass().from_py(val) + @hypothesis.given(val=strategies.one_of(strategies.integers(), + strategies.text())) + def test_hypothesis(self, klass, val): + try: + klass().from_py(val) + except configexc.ValidationError: + pass + class TestCommand: @@ -1187,7 +1246,6 @@ class TestRegex: class TestDict: - # FIXME:conf make sure from_str/from_py is called on keytype/valtype. # FIXME:conf how to handle {}? @pytest.fixture @@ -1202,21 +1260,26 @@ class TestDict: def test_from_str_valid(self, klass, val): expected = None if not val else val if isinstance(val, dict): - val = json.dumps(val) + val = utils.yaml_dump(val) d = klass(keytype=configtypes.String(), valtype=configtypes.String(), none_ok=True) assert d.from_str(val) == expected @pytest.mark.parametrize('val', [ - '["foo"]', # valid json but not a dict + '["foo"]', # valid yaml but not a dict '{"hello": 23}', # non-string as value - '[invalid', # invalid json + '[invalid', # invalid yaml ]) def test_from_str_invalid(self, klass, val): d = klass(keytype=configtypes.String(), valtype=configtypes.String()) with pytest.raises(configexc.ValidationError): d.from_str(val) + def test_from_str_int(self): + typ = configtypes.Dict(keytype=configtypes.Int(), + valtype=configtypes.Int()) + assert typ.from_str(utils.yaml_dump({0: 0})) == {0: 0} + @pytest.mark.parametrize('val', [ {"one": "1"}, # missing key {"one": "1", "two": "2", "three": "3"}, # extra key @@ -1228,10 +1291,27 @@ class TestDict: with pytest.raises(configexc.ValidationError): if from_str: - d.from_str(json.dumps(val)) + d.from_str(utils.yaml_dump(val)) else: d.from_py(val) + @hypothesis.given(val=strategies.dictionaries(strategies.booleans(), + strategies.booleans())) + def test_hypothesis(self, klass, val): + d = klass(keytype=configtypes.Bool(), + valtype=configtypes.Bool()) + d.from_py(val) + + @hypothesis.given(val=strategies.dictionaries(strategies.booleans(), + strategies.booleans())) + def test_hypothesis_text(self, klass, val): + text = utils.yaml_dump(val).decode('utf-8') + d = klass(keytype=configtypes.Bool(), valtype=configtypes.Bool()) + try: + d.from_str(text) + except configexc.ValidationError: + pass + def unrequired_class(**kwargs): return configtypes.File(required=False, **kwargs) @@ -1588,7 +1668,7 @@ class TestConfirmQuit: def test_from_py_valid(self, klass, val): cq = klass(none_ok=True) assert cq.from_py(val) == val - assert cq.from_str(json.dumps(val)) == val + assert cq.from_str(utils.yaml_dump(val)) == val @pytest.mark.parametrize('val', [ ['foo'], From dfee8574662b3d9340d79089e3217c320e8bfc15 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 14:01:07 +0200 Subject: [PATCH 062/516] Make utils.yaml_dump return str --- qutebrowser/utils/utils.py | 10 +++++++--- tests/unit/config/test_configtypes.py | 8 ++++---- tests/unit/utils/test_utils.py | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 95d3c9468..632773b7f 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -889,6 +889,10 @@ def yaml_load(f): def yaml_dump(data, f=None): - """Wrapper over yaml.dump using the C dumper if possible.""" - return yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False, - encoding='utf-8', allow_unicode=True) + """Wrapper over yaml.dump using the C dumper if possible. + + Also returns a str instead of bytes. + """ + yaml_data = yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False, + encoding='utf-8', allow_unicode=True) + return yaml_data.decode('utf-8') diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 4f20cb818..ce4e052db 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -531,7 +531,7 @@ class TestList: @hypothesis.given(val=strategies.lists(strategies.just('foo'))) def test_hypothesis_text(self, klass, val): - text = utils.yaml_dump(val).decode('utf-8') + text = utils.yaml_dump(val) try: klass().from_str(text) except configexc.ValidationError: @@ -746,7 +746,7 @@ class TestInt: @hypothesis.given(val=strategies.integers()) def test_hypothesis_text(self, klass, val): - text = utils.yaml_dump(val).decode('utf-8') + text = utils.yaml_dump(val) try: klass().from_str(text) except configexc.ValidationError: @@ -800,7 +800,7 @@ class TestFloat: @hypothesis.given(val=strategies.one_of(strategies.floats(), strategies.integers())) def test_hypothesis_text(self, klass, val): - text = utils.yaml_dump(val).decode('utf-8') + text = utils.yaml_dump(val) try: klass().from_str(text) except configexc.ValidationError: @@ -1305,7 +1305,7 @@ class TestDict: @hypothesis.given(val=strategies.dictionaries(strategies.booleans(), strategies.booleans())) def test_hypothesis_text(self, klass, val): - text = utils.yaml_dump(val).decode('utf-8') + text = utils.yaml_dump(val) d = klass(keytype=configtypes.Bool(), valtype=configtypes.Bool()) try: d.from_str(text) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index aff5b49b4..d5585c06e 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -943,4 +943,4 @@ def test_yaml_load(): def test_yaml_dump(): - assert utils.yaml_dump([1, 2]) == b'- 1\n- 2\n' + assert utils.yaml_dump([1, 2]) == '- 1\n- 2\n' From 4e729bb9ecff88abf6e87fe4b403ee7643de37be Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 14:04:35 +0200 Subject: [PATCH 063/516] Back to using json in test_configtypes It returns one-line data and is YAML compatible --- tests/unit/config/test_configtypes.py | 33 ++++++++++++--------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index ce4e052db..c1ca352e7 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -21,6 +21,7 @@ import re import os import sys +import json import collections import itertools import warnings @@ -474,12 +475,12 @@ class TestList: @pytest.mark.parametrize('val', [['foo'], ['foo', 'bar']]) def test_from_str(self, klass, val): - yaml_val = utils.yaml_dump(val) - assert klass().from_str(yaml_val) == val + json_val = json.dumps(val) + assert klass().from_str(json_val) == val def test_from_str_int(self): typ = configtypes.List(valtype=configtypes.Int()) - assert typ.from_str(utils.yaml_dump([0])) == [0] + assert typ.from_str(json.dumps([0])) == [0] @pytest.mark.parametrize('val', ['[[', 'true']) def test_from_str_invalid(self, klass, val): @@ -531,7 +532,7 @@ class TestList: @hypothesis.given(val=strategies.lists(strategies.just('foo'))) def test_hypothesis_text(self, klass, val): - text = utils.yaml_dump(val) + text = json.dumps(val) try: klass().from_str(text) except configexc.ValidationError: @@ -746,11 +747,8 @@ class TestInt: @hypothesis.given(val=strategies.integers()) def test_hypothesis_text(self, klass, val): - text = utils.yaml_dump(val) - try: - klass().from_str(text) - except configexc.ValidationError: - pass + text = json.dumps(val) + klass().from_str(text) class TestFloat: @@ -800,11 +798,8 @@ class TestFloat: @hypothesis.given(val=strategies.one_of(strategies.floats(), strategies.integers())) def test_hypothesis_text(self, klass, val): - text = utils.yaml_dump(val) - try: - klass().from_str(text) - except configexc.ValidationError: - pass + text = json.dumps(val) + klass().from_str(text) class TestPerc: @@ -1260,7 +1255,7 @@ class TestDict: def test_from_str_valid(self, klass, val): expected = None if not val else val if isinstance(val, dict): - val = utils.yaml_dump(val) + val = json.dumps(val) d = klass(keytype=configtypes.String(), valtype=configtypes.String(), none_ok=True) assert d.from_str(val) == expected @@ -1278,7 +1273,7 @@ class TestDict: def test_from_str_int(self): typ = configtypes.Dict(keytype=configtypes.Int(), valtype=configtypes.Int()) - assert typ.from_str(utils.yaml_dump({0: 0})) == {0: 0} + assert typ.from_str('{0: 0}') == {0: 0} @pytest.mark.parametrize('val', [ {"one": "1"}, # missing key @@ -1291,7 +1286,7 @@ class TestDict: with pytest.raises(configexc.ValidationError): if from_str: - d.from_str(utils.yaml_dump(val)) + d.from_str(json.dumps(val)) else: d.from_py(val) @@ -1305,7 +1300,7 @@ class TestDict: @hypothesis.given(val=strategies.dictionaries(strategies.booleans(), strategies.booleans())) def test_hypothesis_text(self, klass, val): - text = utils.yaml_dump(val) + text = json.dumps(val) d = klass(keytype=configtypes.Bool(), valtype=configtypes.Bool()) try: d.from_str(text) @@ -1668,7 +1663,7 @@ class TestConfirmQuit: def test_from_py_valid(self, klass, val): cq = klass(none_ok=True) assert cq.from_py(val) == val - assert cq.from_str(utils.yaml_dump(val)) == val + assert cq.from_str(json.dumps(val)) == val @pytest.mark.parametrize('val', [ ['foo'], From 001312ca822b8a813774a4626e0c396c89948749 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 14:09:43 +0200 Subject: [PATCH 064/516] Disallow Booleans for configtypes.Int.from_py --- qutebrowser/config/configtypes.py | 12 ++++++++---- tests/unit/config/test_configtypes.py | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index c39fdceb7..632f2311c 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -120,10 +120,14 @@ class BaseType: value: The value to check. pytype: A Python type to check the value against. """ - if value is None and not self.none_ok: - raise configexc.ValidationError(value, "may not be null!") + if value is None: + if not self.none_ok: + raise configexc.ValidationError(value, "may not be null!") + else: + return - if value is not None and not isinstance(value, pytype): + if (not isinstance(value, pytype) or + pytype is int and isinstance(value, bool)): if isinstance(pytype, tuple): expected = ' or '.join(typ.__name__ for typ in pytype) else: @@ -132,7 +136,7 @@ class BaseType: value, "expected a value of type {} but got {}.".format( expected, type(value).__name__)) - if value is not None and isinstance(value, str): + if isinstance(value, str): self._basic_str_validation(value) def _basic_str_validation(self, value): diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index c1ca352e7..c15f2e09d 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -736,6 +736,7 @@ class TestInt: ({}, 2.5), ({}, 'foobar'), ({'minval': 2, 'maxval': 3}, 1), + ({}, True), ]) def test_from_py_invalid(self, klass, kwargs, val): with pytest.raises(configexc.ValidationError): From 6733e92b50cc891ea5084dc3799d94215b3db4b5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 14:14:54 +0200 Subject: [PATCH 065/516] Handle files correctly in utils.yaml_dump --- qutebrowser/utils/utils.py | 5 ++++- tests/unit/utils/test_utils.py | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 632773b7f..f6b1d5a9f 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -895,4 +895,7 @@ def yaml_dump(data, f=None): """ yaml_data = yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False, encoding='utf-8', allow_unicode=True) - return yaml_data.decode('utf-8') + if yaml_data is None: + return None + else: + return yaml_data.decode('utf-8') diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index d5585c06e..4bec8f57c 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -938,9 +938,22 @@ def test_expand_windows_drive(path, expected): assert utils.expand_windows_drive(path) == expected -def test_yaml_load(): - assert utils.yaml_load("[1, 2]") == [1, 2] +class TestYaml: + def test_load(self): + assert utils.yaml_load("[1, 2]") == [1, 2] -def test_yaml_dump(): - assert utils.yaml_dump([1, 2]) == '- 1\n- 2\n' + def test_load_file(self, tmpdir): + tmpfile = tmpdir / 'foo.yml' + tmpfile.write('[1, 2]') + with tmpfile.open(encoding='utf-8') as f: + assert utils.yaml_load(f) == [1, 2] + + def test_dump(self): + assert utils.yaml_dump([1, 2]) == '- 1\n- 2\n' + + def test_dump_file(self, tmpdir): + tmpfile = tmpdir / 'foo.yml' + with tmpfile.open('w', encoding='utf-8') as f: + utils.yaml_dump([1, 2], f) + assert tmpfile.read() == '- 1\n- 2\n' From a1ed81f7907c36be2444ce813604aba4fdd5901b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 14:18:53 +0200 Subject: [PATCH 066/516] Patch out setting completion Let's bring it back with the completion refactoring --- qutebrowser/completion/models/instances.py | 30 +--------------------- qutebrowser/config/config.py | 3 --- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/qutebrowser/completion/models/instances.py b/qutebrowser/completion/models/instances.py index c686e7fa5..f515e7e25 100644 --- a/qutebrowser/completion/models/instances.py +++ b/qutebrowser/completion/models/instances.py @@ -27,9 +27,8 @@ Module attributes: import functools -from qutebrowser.completion.models import miscmodels, urlmodel, configmodel +from qutebrowser.completion.models import miscmodels, urlmodel from qutebrowser.utils import objreg, usertypes, log, debug -from qutebrowser.config import configdata, config _instances = {} @@ -65,22 +64,6 @@ def _init_tab_completion(): _instances[usertypes.Completion.tab] = model -def _init_setting_completions(): - """Initialize setting completion models.""" - log.completion.debug("Initializing setting completion.") - _instances[usertypes.Completion.section] = ( - configmodel.SettingSectionCompletionModel()) - _instances[usertypes.Completion.option] = {} - _instances[usertypes.Completion.value] = {} - for sectname in configdata.DATA: - opt_model = configmodel.SettingOptionCompletionModel(sectname) - _instances[usertypes.Completion.option][sectname] = opt_model - _instances[usertypes.Completion.value][sectname] = {} - for opt in configdata.DATA[sectname]: - val_model = configmodel.SettingValueCompletionModel(sectname, opt) - _instances[usertypes.Completion.value][sectname][opt] = val_model - - def init_quickmark_completions(): """Initialize quickmark completion models.""" log.completion.debug("Initializing quickmark completion.") @@ -126,9 +109,6 @@ INITIALIZERS = { usertypes.Completion.helptopic: _init_helptopic_completion, usertypes.Completion.url: _init_url_completion, usertypes.Completion.tab: _init_tab_completion, - usertypes.Completion.section: _init_setting_completions, - usertypes.Completion.option: _init_setting_completions, - usertypes.Completion.value: _init_setting_completions, usertypes.Completion.quickmark_by_name: init_quickmark_completions, usertypes.Completion.bookmark_by_url: init_bookmark_completions, usertypes.Completion.sessions: init_session_completion, @@ -163,12 +143,6 @@ def update(completions): did_run.append(func) -@config.change_filter('aliases', function=True) -def _update_aliases(): - """Update completions that include command aliases.""" - update([usertypes.Completion.command]) - - def init(): """Initialize completions. Note this only connects signals.""" quickmark_manager = objreg.get('quickmark-manager') @@ -192,5 +166,3 @@ def init(): functools.partial(update, [usertypes.Completion.command])) keyconf.changed.connect( functools.partial(update, [usertypes.Completion.bind])) - - config.instance.changed.connect(_update_aliases) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 8a942632b..56f97f762 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -722,9 +722,6 @@ class ConfigManager(QObject): e.__class__.__name__, e)) @cmdutils.register(name='set', instance='config', star_args_optional=True) - @cmdutils.argument('section_', completion=Completion.section) - @cmdutils.argument('option', completion=Completion.option) - @cmdutils.argument('values', completion=Completion.value) @cmdutils.argument('win_id', win_id=True) def set_command(self, win_id, section_=None, option=None, *values, temp=False, print_=False): From 36a5614c61f1443d557e000a2b2882623f6599db Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 14:23:04 +0200 Subject: [PATCH 067/516] Add "FIXME" --- qutebrowser/config/configdata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 7d36a499a..17bd3671c 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -68,6 +68,7 @@ FIRST_COMMENT = r""" """ +# FIXME:conf where to put those? SECTION_DESC = { 'general': "General/miscellaneous options.", 'ui': "General options related to the user interface.", From 3a6bcb3dd0873776a5002c6bd55754634ae94e5c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Jun 2017 14:31:21 +0200 Subject: [PATCH 068/516] Remove icon from base.html --- qutebrowser/browser/qutescheme.py | 6 ++---- qutebrowser/browser/webengine/webenginetab.py | 2 +- qutebrowser/browser/webengine/webview.py | 2 +- qutebrowser/browser/webkit/network/filescheme.py | 5 ++--- qutebrowser/browser/webkit/webpage.py | 2 +- qutebrowser/html/base.html | 3 --- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- 7 files changed, 8 insertions(+), 14 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 83056e396..0880a247c 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -123,8 +123,7 @@ class add_handler: # pylint: disable=invalid-name title="Error while opening qute://url", url=url.toDisplayString(), error='{} is not available with this ' - 'backend'.format(url.toDisplayString()), - icon='') + 'backend'.format(url.toDisplayString())) return 'text/html', html @@ -415,8 +414,7 @@ def qute_help(url): "properly. If you are running qutebrowser from the git " "repository, please run scripts/asciidoc2html.py. " "If you're running a released version this is a bug, please " - "use :report to report it.", - icon='') + "use :report to report it.") return 'text/html', html urlpath = url.path() if not urlpath or urlpath == '/': diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 1345e8001..fa443918f 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -693,7 +693,7 @@ class WebEngineTab(browsertab.AbstractTab): error_page = jinja.render( 'error.html', title="Error loading page: {}".format(url_string), - url=url_string, error="Authentication required", icon='') + url=url_string, error="Authentication required") self.set_html(error_page) @pyqtSlot('QWebEngineFullScreenRequest') diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index fcad18206..60ea06914 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -212,7 +212,7 @@ class WebEnginePage(QWebEnginePage): url_string = url.toDisplayString() error_page = jinja.render( 'error.html', title="Error loading page: {}".format(url_string), - url=url_string, error=str(error), icon='') + url=url_string, error=str(error)) if error.is_overridable(): ignore = shared.ignore_certificate_errors( diff --git a/qutebrowser/browser/webkit/network/filescheme.py b/qutebrowser/browser/webkit/network/filescheme.py index 2f3a3ff18..e379b6738 100644 --- a/qutebrowser/browser/webkit/network/filescheme.py +++ b/qutebrowser/browser/webkit/network/filescheme.py @@ -101,13 +101,12 @@ def dirbrowser_html(path): except OSError as e: html = jinja.render('error.html', title="Error while reading directory", - url='file:///{}'.format(path), error=str(e), - icon='') + url='file:///{}'.format(path), error=str(e)) return html.encode('UTF-8', errors='xmlcharrefreplace') files = get_file_list(path, all_files, os.path.isfile) directories = get_file_list(path, all_files, os.path.isdir) - html = jinja.render('dirbrowser.html', title=title, url=path, icon='', + html = jinja.render('dirbrowser.html', title=title, url=path, parent=parent, files=files, directories=directories) return html.encode('UTF-8', errors='xmlcharrefreplace') diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 70750eef8..59d91616e 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -170,7 +170,7 @@ class BrowserPage(QWebPage): title = "Error loading page: {}".format(urlstr) error_html = jinja.render( 'error.html', - title=title, url=urlstr, error=error_str, icon='') + title=title, url=urlstr, error=error_str) errpage.content = error_html.encode('utf-8') errpage.encoding = 'utf-8' return True diff --git a/qutebrowser/html/base.html b/qutebrowser/html/base.html index 699740025..f21eb227b 100644 --- a/qutebrowser/html/base.html +++ b/qutebrowser/html/base.html @@ -7,9 +7,6 @@ vim: ft=html fileencoding=utf-8 sts=4 sw=4 et: {{ title }} - {% if icon %} - - {% endif %}

    {{ section }}

    {{ config.SECTION_DESC.get(section)|wordwrap(width=120) }}