diff options
67 files changed, 1343 insertions, 685 deletions
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 34d3f71b6..cc44bffd7 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -21,16 +21,28 @@ v1.9.0 (unreleased) Added ~~~~~ +- Initial support for Qt 5.14. - New `tabs.tooltips` setting which can be used to disable hover tooltips for tabs. - New settings to configure the appearance of context menus: - `fonts.contextmenu` - - `colors.contextmenu.bg` - - `colors.contextmenu.fg` + - `colors.contextmenu.menu.bg` + - `colors.contextmenu.menu.fg` + - `colors.contextmenu.active.bg` + - `colors.contextmenu.active.fg` +- New `content.site_specific_quirks` setting which enables workarounds for + websites with broken user agent parsing (enabled by default, see the "Fixed" + section for fixed websites). +- New `qt.force_platformtheme` setting to force Qt to use a given platform + theme. Changed ~~~~~~~ +- The `content.headers.user_agent` setting now is a format string with the + default value resembling the behavior of it being set to null before. + This slightly changes the sent user agent for QtWebKit: Instead of mentioning + qutebrowser and its version it now mentions the Qt version. - The `qute-pass` userscript now has a new `--extra-url-suffixes` (`-s`) argument which passes extra URL suffixes to the tldextract library. - A stack is now used for `:tab-focus last` rather than just saving one tab. @@ -56,6 +68,14 @@ Changed a timeout) on PyQt 5.13.1 and newer. - The `:spawn` command has a new `-m` / `--output-messages` argument which shows qutebrowser messages based on a command's standard output/error. +- Improved insert mode detection for some CodeMirror usages (e.g. in + JupyterLab and Jupyter Notebook). +- If JavaScript is disabled globally, `file://*` now doesn't automatically have + it enabled anymore. Run `:set -u file://* content.javascript.enabled true` to + restore the previous behavior. +- Settings with URL patterns can now be used to affect the behavior of the + QtWebEngine inspector. Note that the underlying URL is `chrome-devtools://*` + from Qt 5.11 to Qt 5.13, but `devtools://*` with Qt 5.14. - Performance improvements for the following areas: * Adding settings with URL patterns * Matching of settings using URL patterns @@ -63,6 +83,9 @@ Changed Fixed ~~~~~ +- Downloads (e.g. via `:download`) now see the same user agent header as + webpages, which fixes cases where overly restrictive servers/WAFs closed the + connection before. - dictcli.py now works correctly on Windows again. - The logic for `:restart` has been revisited which should fix issues with relative basedirs. @@ -70,6 +93,13 @@ Fixed QtWebKit. - Workaround for a Qt bug where a page never finishes loading with a non-overridable TLS error (e.g. due to HSTS). +- The `qute://configdiff` page now doesn't show built-in settings (e.g. + javascript being enabled for `qute://` and `chrome://` pages) anymore. +- The `qute-lastpass` userscript now stops prompting for passwords when + cancelling the password input. +- The tab hover text now shows ampersands (&) correctly. +- With QtWebEngine and Qt >= 5.11, the inspector now shows its icons correctly + even if loading of images is disabled via the `content.images` setting. - Various improvements for URL/searchengine detection: - Strings with a dot but with characters not allowed in a URL (e.g. an underscore) are now not treated as URL anymore. @@ -81,6 +111,12 @@ Fixed - `url.open_base_url = True` together with `url.auto_search = 'never'` is now handled correctly. - Fixed crash when a search engine URL turns out to be invalid. +- Site specific quirks which work around some broken websites: + - WhatsApp Web + - Google Accounts + - Slack (with older QtWebEngine versions) + - Dell.com support pages (with Qt 5.7) + - Google Docs (fixes broken IME/compose key) v1.8.3 (2019-12-05) ------------------- diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index 95c53cea5..ba984f328 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -105,7 +105,7 @@ Useful utilities Checkers ~~~~~~~~ -qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its +qutebrowser uses https://tox.readthedocs.io/en/latest/[tox] to run its unittests and several linters/checkers. Currently, the following tox environments are available: diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index 9a67fcb56..b4a7c8602 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -96,15 +96,10 @@ Those were handled appropriately security bugs, please contact me directly at mail@qutebrowser.org, GPG ID https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072]. -Is there an adblocker?:: - There is a host-based adblocker which takes /etc/hosts-like lists. A "real" - adblocker has a - https://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big - impact] on browsing speed and - https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM - usage], so implementing support for AdBlockPlus-like lists is currently not - a priority. - +Is there an ad blocker?:: + There is a simple host-based ad blocker that takes `/etc/hosts`-like lists. ++ +More advanced ad blockers can have a big impact on browsing speed and https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM usage], so implementing support for AdBlock Plus-like lists is not a priority. How can I get No-Script-like behavior?:: To disable JavaScript by default: + diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index b95511245..9c920a55e 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -432,6 +432,7 @@ Various emacs/conkeror-like keybinding configs exist: - https://gitlab.com/jgkamat/qutemacs/blob/master/qutemacs.py[jgkamat] - https://gitlab.com/Kaligule/qutebrowser-emacs-config/blob/master/config.py[Kaligule] - http://me0w.net/pit/1540882719[nm0i] +- https://www.reddit.com/r/qutebrowser/comments/eh10i7/config_share_qute_with_emacs_keybindings/[jasonsun0310] It's also mostly possible to get rid of modal keybindings by setting `input.insert_mode.auto_enter` to `false`, and `input.forward_unbound_keys` to diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 355c30153..7cab1dbe9 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -32,8 +32,10 @@ |<<colors.completion.odd.bg,colors.completion.odd.bg>>|Background color of the completion widget for odd rows. |<<colors.completion.scrollbar.bg,colors.completion.scrollbar.bg>>|Color of the scrollbar in the completion view. |<<colors.completion.scrollbar.fg,colors.completion.scrollbar.fg>>|Color of the scrollbar handle in the completion view. -|<<colors.contextmenu.bg,colors.contextmenu.bg>>|Background color of the context menu. -|<<colors.contextmenu.fg,colors.contextmenu.fg>>|Foreground color of the context menu. +|<<colors.contextmenu.menu.bg,colors.contextmenu.menu.bg>>|Background color of the context menu. +|<<colors.contextmenu.menu.fg,colors.contextmenu.menu.fg>>|Foreground color of the context menu. +|<<colors.contextmenu.selected.bg,colors.contextmenu.selected.bg>>|Background color of the context menu's selected item. +|<<colors.contextmenu.selected.fg,colors.contextmenu.selected.fg>>|Foreground color of the context menu's selected item. |<<colors.downloads.bar.bg,colors.downloads.bar.bg>>|Background color for the download bar. |<<colors.downloads.error.bg,colors.downloads.error.bg>>|Background color for downloads with errors. |<<colors.downloads.error.fg,colors.downloads.error.fg>>|Foreground color for downloads with errors. @@ -138,7 +140,7 @@ |<<content.headers.custom,content.headers.custom>>|Custom headers for qutebrowser HTTP requests. |<<content.headers.do_not_track,content.headers.do_not_track>>|Value to send in the `DNT` header. |<<content.headers.referer,content.headers.referer>>|When to send the Referer header. -|<<content.headers.user_agent,content.headers.user_agent>>|User agent to send. Unset to send the default. +|<<content.headers.user_agent,content.headers.user_agent>>|User agent to send. |<<content.host_blocking.enabled,content.host_blocking.enabled>>|Enable host blocking. |<<content.host_blocking.lists,content.host_blocking.lists>>|List of URLs of lists which contain hosts to block. |<<content.host_blocking.whitelist,content.host_blocking.whitelist>>|A list of patterns that should always be loaded, despite being ad-blocked. @@ -168,6 +170,7 @@ |<<content.proxy,content.proxy>>|Proxy to use. |<<content.proxy_dns_requests,content.proxy_dns_requests>>|Send DNS requests over the configured proxy. |<<content.register_protocol_handler,content.register_protocol_handler>>|Allow websites to register protocol handlers via `navigator.registerProtocolHandler`. +|<<content.site_specific_quirks,content.site_specific_quirks>>|Enable quirks (such as faked user agent headers) needed to get specific sites to work properly. |<<content.ssl_strict,content.ssl_strict>>|Validate SSL handshakes. |<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use. |<<content.webgl,content.webgl>>|Enable WebGL. @@ -181,7 +184,7 @@ |<<downloads.open_dispatcher,downloads.open_dispatcher>>|Default program used to open downloads. |<<downloads.position,downloads.position>>|Where to show the downloaded files. |<<downloads.remove_finished,downloads.remove_finished>>|Duration (in milliseconds) to wait before removing finished downloads. -|<<editor.command,editor.command>>|Editor (and arguments) to use for the `open-editor` command. The following placeholders are defined: +|<<editor.command,editor.command>>|Editor (and arguments) to use for the `open-editor` command. |<<editor.encoding,editor.encoding>>|Encoding to use for the editor. |<<fonts.completion.category,fonts.completion.category>>|Font used in the completion categories. |<<fonts.completion.entry,fonts.completion.entry>>|Font used in the completion widget. @@ -244,6 +247,7 @@ |<<prompt.radius,prompt.radius>>|Rounding radius (in pixels) for the edges of prompts. |<<qt.args,qt.args>>|Additional arguments to pass to Qt, without leading `--`. |<<qt.force_platform,qt.force_platform>>|Force a Qt platform to use. +|<<qt.force_platformtheme,qt.force_platformtheme>>|Force a Qt platformtheme to use. |<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine. |<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling. |<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode. @@ -858,8 +862,8 @@ Type: <<types,QssColor>> Default: +pass:[white]+ -[[colors.contextmenu.bg]] -=== colors.contextmenu.bg +[[colors.contextmenu.menu.bg]] +=== colors.contextmenu.menu.bg Background color of the context menu. If set to null, the Qt default is used. @@ -867,8 +871,8 @@ Type: <<types,QssColor>> Default: empty -[[colors.contextmenu.fg]] -=== colors.contextmenu.fg +[[colors.contextmenu.menu.fg]] +=== colors.contextmenu.menu.fg Foreground color of the context menu. If set to null, the Qt default is used. @@ -876,6 +880,24 @@ Type: <<types,QssColor>> Default: empty +[[colors.contextmenu.selected.bg]] +=== colors.contextmenu.selected.bg +Background color of the context menu's selected item. +If set to null, the Qt default is used. + +Type: <<types,QssColor>> + +Default: empty + +[[colors.contextmenu.selected.fg]] +=== colors.contextmenu.selected.fg +Foreground color of the context menu's selected item. +If set to null, the Qt default is used. + +Type: <<types,QssColor>> + +Default: empty + [[colors.downloads.bar.bg]] === colors.downloads.bar.bg Background color for the download bar. @@ -1840,14 +1862,30 @@ Default: +pass:[same-domain]+ [[content.headers.user_agent]] === content.headers.user_agent -User agent to send. Unset to send the default. +User agent to send. + +The following placeholders are defined: + +* `{os_info}`: Something like "X11; Linux x86_64". +* `{webkit_version}`: The underlying WebKit version (set to a fixed value + with QtWebEngine). +* `{qt_key}`: "Qt" for QtWebKit, "QtWebEngine" for QtWebEngine. +* `{qt_version}`: The underlying Qt version. +* `{upstream_browser_key}`: "Version" for QtWebKit, "Chrome" for QtWebEngine. +* `{upstream_browser_version}`: The corresponding Safari/Chrome version. +* `{qutebrowser_version}`: The currently running qutebrowser version. + +The default value is equal to the unchanged user agent of +QtWebKit/QtWebEngine. + Note that the value read from JavaScript is always the global value. + This setting supports URL patterns. -Type: <<types,String>> +Type: <<types,FormatString>> -Default: empty +Default: +pass:[Mozilla/5.0 ({os_info}) AppleWebKit/{webkit_version} (KHTML, like Gecko) {qt_key}/{qt_version} {upstream_browser_key}/{upstream_browser_version} Safari/{webkit_version}]+ [[content.host_blocking.enabled]] === content.host_blocking.enabled @@ -2206,6 +2244,15 @@ On QtWebEngine, this setting requires Qt 5.11 or newer. On QtWebKit, this setting is unavailable. +[[content.site_specific_quirks]] +=== content.site_specific_quirks +Enable quirks (such as faked user agent headers) needed to get specific sites to work properly. +This setting requires a restart. + +Type: <<types,Bool>> + +Default: +pass:[true]+ + [[content.ssl_strict]] === content.ssl_strict Validate SSL handshakes. @@ -2355,8 +2402,15 @@ Default: +pass:[-1]+ [[editor.command]] === editor.command -Editor (and arguments) to use for the `open-editor` command. The following placeholders are defined: -* `{file}`: Filename of the file to be edited. * `{line}`: Line in which the caret is found in the text. * `{column}`: Column in which the caret is found in the text. * `{line0}`: Same as `{line}`, but starting from index 0. * `{column0}`: Same as `{column}`, but starting from index 0. +Editor (and arguments) to use for the `open-editor` command. +The following placeholders are defined: + +* `{file}`: Filename of the file to be edited. +* `{line}`: Line in which the caret is found in the text. +* `{column}`: Column in which the caret is found in the text. +* `{line0}`: Same as `{line}`, but starting from index 0. +* `{column0}`: Same as `{column}`, but starting from index 0. + Type: <<types,ShellCommand>> @@ -3003,6 +3057,16 @@ Type: <<types,String>> Default: empty +[[qt.force_platformtheme]] +=== qt.force_platformtheme +Force a Qt platformtheme to use. +This sets the `QT_QPA_PLATFORMTHEME` environment variable which controls dialogs like the filepicker. By default, Qt determines the platform theme based on the desktop environment. +This setting requires a restart. + +Type: <<types,String>> + +Default: empty + [[qt.force_software_rendering]] === qt.force_software_rendering Force software rendering for QtWebEngine. diff --git a/misc/requirements/requirements-pyqt-5.14.txt b/misc/requirements/requirements-pyqt-5.14.txt new file mode 100644 index 000000000..5c713f7b4 --- /dev/null +++ b/misc/requirements/requirements-pyqt-5.14.txt @@ -0,0 +1,5 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +PyQt5==5.14.0 # rq.filter: < 5.15 +PyQt5-sip==12.7.0 +PyQtWebEngine==5.14.0 # rq.filter: < 5.15 diff --git a/misc/requirements/requirements-pyqt-5.14.txt-raw b/misc/requirements/requirements-pyqt-5.14.txt-raw new file mode 100644 index 000000000..9dadfc846 --- /dev/null +++ b/misc/requirements/requirements-pyqt-5.14.txt-raw @@ -0,0 +1,4 @@ +#@ filter: PyQt5 < 5.15 +#@ filter: PyQtWebEngine < 5.15 +PyQt5 >= 5.14, < 5.15 +PyQtWebEngine >= 5.14, < 5.15 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index b616d29e1..1c1a8aabd 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.13.2 +PyQt5==5.14.0 PyQt5-sip==12.7.0 -PyQtWebEngine==5.13.2 +PyQtWebEngine==5.14.0 diff --git a/misc/userscripts/qute-lastpass b/misc/userscripts/qute-lastpass index ea88cf86f..6845a4cda 100755 --- a/misc/userscripts/qute-lastpass +++ b/misc/userscripts/qute-lastpass @@ -19,7 +19,7 @@ # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. """ -Insert login information using lastpass CLI and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...). +Insert login information using lastpass CLI and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...). A short demonstration can be seen here: https://i.imgur.com/zA61NrF.gifv. """ @@ -85,15 +85,12 @@ def pass_(domain, encoding): args = ['lpass', 'show', '-x', '-j', '-G', '.*{:s}.*'.format(domain)] process = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + candidates = json.loads(process.stdout.decode(encoding).strip() or '[]') err = process.stderr.decode(encoding).strip() - if err: - msg = "LastPass CLI returned for {:s} - {:s}".format(domain, err) - stderr(msg) - return '[]' + if 'could not find specified account' in err.lower(): + return candidates, '' - out = process.stdout.decode(encoding).strip() - - return out + return candidates, err def dmenu(items, invocation, encoding): command = shlex.split(invocation) @@ -121,7 +118,11 @@ def main(arguments): # the URL represents candidates = [] for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.subdomain + extract_result.domain, extract_result.domain, extract_result.ipv4]): - target_candidates = json.loads(pass_(target, arguments.io_encoding)) + target_candidates, err = pass_(target, arguments.io_encoding) + if err: + stderr("LastPass CLI returned for {:s} - {:s}".format(target, err)) + return ExitCodes.FAILURE + if not target_candidates: continue diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 155421952..d429639f3 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -821,14 +821,6 @@ class AbstractTabPrivate: """ raise NotImplementedError - def user_agent(self) -> typing.Optional[str]: - """Get the user agent for this tab. - - This is only implemented for QtWebKit. - For QtWebEngine, always returns None. - """ - raise NotImplementedError - def shutdown(self) -> None: raise NotImplementedError diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f4cabaccd..ab7e60aed 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1273,7 +1273,6 @@ class CommandDispatcher: target = downloads.FileDownloadTarget(dest) tab = self._current_widget() - user_agent = tab.private_api.user_agent() if url: if mhtml_: @@ -1281,7 +1280,7 @@ class CommandDispatcher: "page as mhtml.") url = urlutils.qurl_from_user_input(url) urlutils.raise_cmdexc_if_invalid(url) - download_manager.get(url, user_agent=user_agent, target=target) + download_manager.get(url, target=target) elif mhtml_: tab = self._current_widget() if tab.backend == usertypes.Backend.QtWebEngine: @@ -1302,7 +1301,6 @@ class CommandDispatcher: download_manager.get( self._current_url(), - user_agent=user_agent, qnam=qnam, target=target, suggested_fn=suggested_fn diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index 91a583d08..b0944046b 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory from qutebrowser.browser import downloads -from qutebrowser.config import config +from qutebrowser.config import stylesheet from qutebrowser.utils import qtutils, utils from qutebrowser.qt import sip @@ -85,7 +85,7 @@ class DownloadView(QListView): super().__init__(parent) if not utils.is_mac: self.setStyle(QStyleFactory.create('Fusion')) - config.set_register_stylesheet(self) + stylesheet.set_register(self) self.setResizeMode(QListView.Adjust) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 257f0bce9..1c61e976d 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -286,12 +286,10 @@ class HintActions: prompt = False if context.rapid else None qnam = context.tab.private_api.networkaccessmanager() - user_agent = context.tab.private_api.user_agent() # FIXME:qtwebengine do this with QtWebEngine downloads? download_manager = objreg.get('qtnetwork-download-manager') - download_manager.get(url, qnam=qnam, user_agent=user_agent, - prompt_download_directory=prompt) + download_manager.get(url, qnam=qnam, prompt_download_directory=prompt) def call_userscript(self, elem: webelem.AbstractWebElement, context: HintContext) -> None: diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index bc76d3daa..779574419 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -29,7 +29,7 @@ import attr from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply -from qutebrowser.config import config +from qutebrowser.config import config, websettings from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug from qutebrowser.browser import downloads from qutebrowser.browser.webkit import http @@ -414,12 +414,11 @@ class DownloadManager(downloads.AbstractDownloadManager): private=config.val.content.private_browsing, parent=self) @pyqtSlot('QUrl') - def get(self, url, *, user_agent=None, **kwargs): + def get(self, url, **kwargs): """Start a download with a link URL. Args: url: The URL to get, as QUrl - user_agent: The UA to set for the request, or None. **kwargs: passed to get_request(). Return: @@ -428,9 +427,11 @@ class DownloadManager(downloads.AbstractDownloadManager): if not url.isValid(): urlutils.invalid_url_error(url, "start download") return None + req = QNetworkRequest(url) - if user_agent is not None: - req.setHeader(QNetworkRequest.UserAgentHeader, user_agent) + user_agent = websettings.user_agent(url) + req.setHeader(QNetworkRequest.UserAgentHeader, user_agent) + return self.get_request(req, **kwargs) def get_mhtml(self, tab, target): diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 59abd8113..10abb6f2d 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -218,6 +218,7 @@ class AbstractWebElement(collections.abc.MutableMapping): 'kix-', # Google Docs editor 'ace_'], # http://ace.c9.io/ 'pre': ['CodeMirror'], + 'span': ['cm-'], # Jupyter Notebook } relevant_classes = classes[self.tag_name()] for klass in self.classes(): @@ -252,7 +253,7 @@ class AbstractWebElement(collections.abc.MutableMapping): 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']: + elif tag in ['div', 'pre', 'span']: return self._is_editable_classes() and not strict return False diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 55badb813..02f75fa0c 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import QUrl, QByteArray from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo) -from qutebrowser.config import config +from qutebrowser.config import websettings from qutebrowser.browser import shared from qutebrowser.utils import utils, log, debug, qtutils from qutebrowser.extensions import interceptors @@ -63,49 +63,65 @@ class WebEngineRequest(interceptors.Request): class RequestInterceptor(QWebEngineUrlRequestInterceptor): """Handle ad blocking and custom headers.""" - # This dict should be from QWebEngine Resource Types to qutebrowser - # extension ResourceTypes. If a ResourceType is added to Qt, this table - # should be updated too. - RESOURCE_TYPES = { - QWebEngineUrlRequestInfo.ResourceTypeMainFrame: - interceptors.ResourceType.main_frame, - QWebEngineUrlRequestInfo.ResourceTypeSubFrame: - interceptors.ResourceType.sub_frame, - QWebEngineUrlRequestInfo.ResourceTypeStylesheet: - interceptors.ResourceType.stylesheet, - QWebEngineUrlRequestInfo.ResourceTypeScript: - interceptors.ResourceType.script, - QWebEngineUrlRequestInfo.ResourceTypeImage: - interceptors.ResourceType.image, - QWebEngineUrlRequestInfo.ResourceTypeFontResource: - interceptors.ResourceType.font_resource, - QWebEngineUrlRequestInfo.ResourceTypeSubResource: - interceptors.ResourceType.sub_resource, - QWebEngineUrlRequestInfo.ResourceTypeObject: - interceptors.ResourceType.object, - QWebEngineUrlRequestInfo.ResourceTypeMedia: - interceptors.ResourceType.media, - QWebEngineUrlRequestInfo.ResourceTypeWorker: - interceptors.ResourceType.worker, - QWebEngineUrlRequestInfo.ResourceTypeSharedWorker: - interceptors.ResourceType.shared_worker, - QWebEngineUrlRequestInfo.ResourceTypePrefetch: - interceptors.ResourceType.prefetch, - QWebEngineUrlRequestInfo.ResourceTypeFavicon: - interceptors.ResourceType.favicon, - QWebEngineUrlRequestInfo.ResourceTypeXhr: - interceptors.ResourceType.xhr, - QWebEngineUrlRequestInfo.ResourceTypePing: - interceptors.ResourceType.ping, - QWebEngineUrlRequestInfo.ResourceTypeServiceWorker: - interceptors.ResourceType.service_worker, - QWebEngineUrlRequestInfo.ResourceTypeCspReport: - interceptors.ResourceType.csp_report, - QWebEngineUrlRequestInfo.ResourceTypePluginResource: - interceptors.ResourceType.plugin_resource, - QWebEngineUrlRequestInfo.ResourceTypeUnknown: - interceptors.ResourceType.unknown, - } + def __init__(self, parent=None): + super().__init__(parent) + # This dict should be from QWebEngine Resource Types to qutebrowser + # extension ResourceTypes. If a ResourceType is added to Qt, this table + # should be updated too. + self._resource_types = { + QWebEngineUrlRequestInfo.ResourceTypeMainFrame: + interceptors.ResourceType.main_frame, + QWebEngineUrlRequestInfo.ResourceTypeSubFrame: + interceptors.ResourceType.sub_frame, + QWebEngineUrlRequestInfo.ResourceTypeStylesheet: + interceptors.ResourceType.stylesheet, + QWebEngineUrlRequestInfo.ResourceTypeScript: + interceptors.ResourceType.script, + QWebEngineUrlRequestInfo.ResourceTypeImage: + interceptors.ResourceType.image, + QWebEngineUrlRequestInfo.ResourceTypeFontResource: + interceptors.ResourceType.font_resource, + QWebEngineUrlRequestInfo.ResourceTypeSubResource: + interceptors.ResourceType.sub_resource, + QWebEngineUrlRequestInfo.ResourceTypeObject: + interceptors.ResourceType.object, + QWebEngineUrlRequestInfo.ResourceTypeMedia: + interceptors.ResourceType.media, + QWebEngineUrlRequestInfo.ResourceTypeWorker: + interceptors.ResourceType.worker, + QWebEngineUrlRequestInfo.ResourceTypeSharedWorker: + interceptors.ResourceType.shared_worker, + QWebEngineUrlRequestInfo.ResourceTypePrefetch: + interceptors.ResourceType.prefetch, + QWebEngineUrlRequestInfo.ResourceTypeFavicon: + interceptors.ResourceType.favicon, + QWebEngineUrlRequestInfo.ResourceTypeXhr: + interceptors.ResourceType.xhr, + QWebEngineUrlRequestInfo.ResourceTypePing: + interceptors.ResourceType.ping, + QWebEngineUrlRequestInfo.ResourceTypeServiceWorker: + interceptors.ResourceType.service_worker, + QWebEngineUrlRequestInfo.ResourceTypeCspReport: + interceptors.ResourceType.csp_report, + QWebEngineUrlRequestInfo.ResourceTypePluginResource: + interceptors.ResourceType.plugin_resource, + QWebEngineUrlRequestInfo.ResourceTypeUnknown: + interceptors.ResourceType.unknown, + } + + try: + preload_main_frame = (QWebEngineUrlRequestInfo. + ResourceTypeNavigationPreloadMainFrame) + preload_sub_frame = (QWebEngineUrlRequestInfo. + ResourceTypeNavigationPreloadSubFrame) + except AttributeError: + # Added in Qt 5.14 + pass + else: + self._resource_types[preload_main_frame] = ( + interceptors.ResourceType.preload_main_frame) + self._resource_types[preload_sub_frame] = ( + interceptors.ResourceType.preload_sub_frame) def install(self, profile): """Install the interceptor on the given QWebEngineProfile.""" @@ -155,8 +171,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): # Per QWebEngineUrlRequestInfo::ResourceType documentation, if we fail # our lookup, we should fall back to ResourceTypeUnknown try: - resource_type = RequestInterceptor.RESOURCE_TYPES[ - info.resourceType()] + resource_type = self._resource_types[info.resourceType()] except KeyError: log.webview.warning( "Resource type {} not found in RequestInterceptor dict." @@ -189,6 +204,5 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): for header, value in shared.custom_headers(url=url): info.setHttpHeader(header, value) - user_agent = config.instance.get('content.headers.user_agent', url=url) - if user_agent is not None: - info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) + user_agent = websettings.user_agent(url) + info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index 4bf72502c..28919643b 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -22,9 +22,10 @@ import os from PyQt5.QtCore import QUrl -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings +from PyQt5.QtWebEngineWidgets import QWebEngineView from qutebrowser.browser import inspector +from qutebrowser.browser.webengine import webenginesettings class WebEngineInspector(inspector.AbstractWebInspector): @@ -35,8 +36,7 @@ class WebEngineInspector(inspector.AbstractWebInspector): super().__init__(parent) self.port = None view = QWebEngineView() - settings = view.settings() - settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) + self._settings = webenginesettings.WebEngineSettings(view.settings()) self._set_widget(view) def _inspect_old(self, page): @@ -47,16 +47,21 @@ class WebEngineInspector(inspector.AbstractWebInspector): raise inspector.WebInspectorError( "QtWebEngine inspector is not enabled. See " "'qutebrowser --help' for details.") - url = QUrl('http://localhost:{}/'.format(port)) + + # We're lying about the URL here a bit, but this way, URL patterns for + # Qt 5.11/5.12/5.13 also work in this case. + self._settings.update_for_url(QUrl('chrome-devtools://devtools')) if page is None: self._widget.load(QUrl('about:blank')) else: - self._widget.load(url) + self._widget.load(QUrl('http://localhost:{}/'.format(port))) def _inspect_new(self, page): """Set up the inspector for Qt >= 5.11.""" - self._widget.page().setInspectedPage(page) + inspector_page = self._widget.page() + inspector_page.setInspectedPage(page) + self._settings.update_for_url(inspector_page.requestedUrl()) def inspect(self, page): try: diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 3cf4a8adc..174564c39 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -35,7 +35,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, from qutebrowser.browser.webengine import spell, webenginequtescheme from qutebrowser.config import config, websettings from qutebrowser.config.websettings import AttributeInfo as Attr -from qutebrowser.utils import utils, standarddir, qtutils, message, log +from qutebrowser.utils import (utils, standarddir, qtutils, message, log, + urlmatch) # The default QWebEngineProfile default_profile = typing.cast(QWebEngineProfile, None) @@ -44,7 +45,7 @@ private_profile = None # type: typing.Optional[QWebEngineProfile] # The global WebEngineSettings object global_settings = typing.cast('WebEngineSettings', None) -default_user_agent = None +parsed_user_agent = None class _SettingsWrapper: @@ -228,7 +229,9 @@ class ProfileSetter: per-domain values), but this one still gets used for things like window.navigator.userAgent/.languages in JS. """ - self._profile.setHttpUserAgent(config.val.content.headers.user_agent) + user_agent = websettings.user_agent() + self._profile.setHttpUserAgent(user_agent) + accept_language = config.val.content.headers.accept_language if accept_language is not None: self._profile.setHttpAcceptLanguage(accept_language) @@ -296,12 +299,22 @@ def _update_settings(option): private_profile.setter.set_dictionary_language(warn=False) +def _init_user_agent_str(ua): + global parsed_user_agent + parsed_user_agent = websettings.UserAgent.parse(ua) + + +def init_user_agent(): + _init_user_agent_str(QWebEngineProfile.defaultProfile().httpUserAgent()) + + def _init_profiles(): """Init the two used QWebEngineProfiles.""" - global default_profile, private_profile, default_user_agent + global default_profile, private_profile default_profile = QWebEngineProfile.defaultProfile() - default_user_agent = default_profile.httpUserAgent() + init_user_agent() + default_profile.setter = ProfileSetter(default_profile) default_profile.setCachePath( os.path.join(standarddir.cache(), 'webengine')) @@ -317,6 +330,50 @@ def _init_profiles(): private_profile.setter.init_profile() +def _init_site_specific_quirks(): + if not config.val.content.site_specific_quirks: + return + + # default_ua = ("Mozilla/5.0 ({os_info}) " + # "AppleWebKit/{webkit_version} (KHTML, like Gecko) " + # "{qt_key}/{qt_version} " + # "{upstream_browser_key}/{upstream_browser_version} " + # "Safari/{webkit_version}") + no_qtwe_ua = ("Mozilla/5.0 ({os_info}) " + "AppleWebKit/{webkit_version} (KHTML, like Gecko) " + "{upstream_browser_key}/{upstream_browser_version} " + "Safari/{webkit_version}") + firefox_ua = "Mozilla/5.0 ({os_info}; rv:71.0) Gecko/20100101 Firefox/71.0" + new_chrome_ua = ("Mozilla/5.0 ({os_info}) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/99 " + "Safari/537.36") + + user_agents = { + 'https://web.whatsapp.com/': no_qtwe_ua, + 'https://accounts.google.com/*': firefox_ua, + 'https://*.slack.com/*': new_chrome_ua, + 'https://docs.google.com/*': firefox_ua, + } + + if not qtutils.version_check('5.9'): + user_agents['https://www.dell.com/support/*'] = new_chrome_ua + + for pattern, ua in user_agents.items(): + config.instance.set_obj('content.headers.user_agent', ua, + pattern=urlmatch.UrlPattern(pattern), + hide_userconfig=True) + + +def _init_devtools_settings(): + """Make sure the devtools always get images/JS permissions.""" + for setting in ['content.javascript.enabled', 'content.images']: + for pattern in ['chrome-devtools://*', 'devtools://*']: + config.instance.set_obj(setting, True, + pattern=urlmatch.UrlPattern(pattern), + hide_userconfig=True) + + def init(args): """Initialize the global QWebSettings.""" if (args.enable_webengine_inspector and @@ -333,6 +390,9 @@ def init(args): global_settings = WebEngineSettings(_SettingsWrapper()) global_settings.init_settings() + _init_site_specific_quirks() + _init_devtools_settings() + def shutdown(): pass diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 3c5eeb545..74b8a49a8 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -27,8 +27,10 @@ Module attributes: import typing import os.path +from PyQt5.QtCore import QUrl from PyQt5.QtGui import QFont from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtWebKitWidgets import QWebPage from qutebrowser.config import config, websettings from qutebrowser.config.websettings import AttributeInfo as Attr @@ -39,6 +41,8 @@ from qutebrowser.browser import shared # The global WebKitSettings object global_settings = typing.cast('WebKitSettings', None) +parsed_user_agent = None + class WebKitSettings(websettings.AbstractSettings): @@ -78,6 +82,8 @@ class WebKitSettings(websettings.AbstractSettings): Attr(QWebSettings.PrintElementBackgrounds), 'content.xss_auditing': Attr(QWebSettings.XSSAuditingEnabled), + 'content.site_specific_quirks': + Attr(QWebSettings.SiteSpecificQuirksEnabled), 'input.spatial_navigation': Attr(QWebSettings.SpatialNavigationEnabled), @@ -160,6 +166,12 @@ def _update_settings(option): _set_cache_maximum_pages(settings) +def _init_user_agent(): + global parsed_user_agent + ua = QWebPage().userAgentForUrl(QUrl()) + parsed_user_agent = websettings.UserAgent.parse(ua) + + def init(_args): """Initialize the global QWebSettings.""" cache_path = standarddir.cache() @@ -178,6 +190,8 @@ def init(_args): _set_cookie_accept_policy(settings) _set_cache_maximum_pages(settings) + _init_user_agent() + config.instance.changed.connect(_update_settings) global global_settings diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 5feb9aee8..1c239ffe3 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -682,10 +682,6 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate): def networkaccessmanager(self): return self._widget.page().networkAccessManager() - def user_agent(self): - page = self._widget.page() - return page.userAgentForUrl(self._tab.url()) - def clear_ssl_errors(self): self.networkaccessmanager().clear_all_ssl_errors() diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 7069c1699..743e550f0 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QFileDialog from PyQt5.QtPrintSupport import QPrintDialog from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame -from qutebrowser.config import config +from qutebrowser.config import websettings from qutebrowser.browser import pdfjs, shared, downloads, greasemonkey from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit.network import networkmanager @@ -411,11 +411,7 @@ class BrowserPage(QWebPage): def userAgentForUrl(self, url): """Override QWebPage::userAgentForUrl to customize the user agent.""" - ua = config.instance.get('content.headers.user_agent', url=url) - if ua is None: - return super().userAgentForUrl(url) - else: - return ua + return websettings.user_agent(url) def supportsExtension(self, ext): """Override QWebPage::supportsExtension to provide error pages. diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 1cd9d0a2a..88daf06aa 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -24,7 +24,7 @@ from PyQt5.QtWidgets import QStyleFactory from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebView, QWebPage -from qutebrowser.config import config +from qutebrowser.config import config, stylesheet from qutebrowser.keyinput import modeman from qutebrowser.utils import log, usertypes, utils, objreg, debug from qutebrowser.browser.webkit import webpage @@ -84,7 +84,7 @@ class WebView(QWebView): self.setPage(page) - config.set_register_stylesheet(self) + stylesheet.set_register(self) def __repr__(self): urlstr = self.url().toDisplayString(QUrl.EncodeUnicode) # type: ignore diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py index 5eb465a24..8398950a9 100644 --- a/qutebrowser/commands/cmdexc.py +++ b/qutebrowser/commands/cmdexc.py @@ -30,12 +30,12 @@ class Error(Exception): class NoSuchCommandError(Error): - """Raised when a command wasn't found.""" + """Raised when a command isn't found.""" class ArgumentTypeError(Error): - """Raised when an argument had an invalid type.""" + """Raised when an argument is an invalid type.""" class PrerequisitesError(Error): @@ -43,5 +43,5 @@ class PrerequisitesError(Error): """Raised when a cmd can't be used because some prerequisites aren't met. This is raised for example when we're in the wrong mode while executing the - command, or we need javascript enabled but don't have done so. + command, or we need javascript enabled but haven't done so. """ diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 439af695c..8ff19c7d8 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -28,7 +28,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier from qutebrowser.utils import message, log, objreg, standarddir, utils from qutebrowser.commands import runners -from qutebrowser.config import config +from qutebrowser.config import websettings from qutebrowser.misc import guiprocess from qutebrowser.browser import downloads @@ -429,10 +429,8 @@ 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.content.headers.user_agent - if user_agent is not None: - env['QUTE_USER_AGENT'] = user_agent + env['QUTE_USER_AGENT'] = websettings.user_agent() env['QUTE_CONFIG_DIR'] = standarddir.config() env['QUTE_DATA_DIR'] = standarddir.data() env['QUTE_DOWNLOAD_DIR'] = downloads.download_dir() diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 4d90a4305..5b1b080e4 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -28,7 +28,7 @@ import typing from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory, QWidget from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize -from qutebrowser.config import config +from qutebrowser.config import config, stylesheet from qutebrowser.completion import completiondelegate from qutebrowser.utils import utils, usertypes, debug, log from qutebrowser.api import cmdutils @@ -125,7 +125,7 @@ class CompletionView(QTreeView): self._delegate = completiondelegate.CompletionItemDelegate(self) self.setItemDelegate(self._delegate) self.setStyle(QStyleFactory.create('Fusion')) - config.set_register_stylesheet(self) + stylesheet.set_register(self) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setHeaderHidden(True) self.setAlternatingRowColors(True) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index fdb55ce4b..41ef6eb3d 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -23,13 +23,13 @@ import copy import contextlib import functools import typing -from typing import Any, Optional, FrozenSet +from typing import Any, Optional -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl +from PyQt5.QtCore import pyqtSignal, QObject, QUrl from qutebrowser.config import configdata, configexc, configutils -from qutebrowser.utils import utils, log, jinja, urlmatch -from qutebrowser.misc import objects, debugcachestats +from qutebrowser.utils import utils, log, urlmatch +from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils if typing.TYPE_CHECKING: @@ -278,7 +278,6 @@ class Config(QObject): yaml_config: 'configfiles.YamlConfig', parent: QObject = None) -> None: super().__init__(parent) - self.changed.connect(_render_stylesheet.cache_clear) self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]] self._yaml = yaml_config self._init_values() @@ -307,7 +306,8 @@ class Config(QObject): def _set_value(self, opt: 'configdata.Option', value: Any, - pattern: urlmatch.UrlPattern = None) -> None: + pattern: urlmatch.UrlPattern = None, + hide_userconfig: bool = False) -> None: """Set the given option to the given value.""" if not isinstance(objects.backend, objects.NoBackend): if objects.backend not in opt.backends: @@ -316,7 +316,8 @@ class Config(QObject): opt.typ.to_py(value) # for validation - self._values[opt.name].add(opt.typ.from_obj(value), pattern) + self._values[opt.name].add(opt.typ.from_obj(value), + pattern, hide_userconfig=hide_userconfig) self.changed.emit(opt.name) log.config.debug("Config option changed: {} = {}".format( @@ -395,7 +396,7 @@ class Config(QObject): """Get the given setting as object (for YAML/config.py). This gets the overridden value for a given pattern, or - configutils.UNSET if no such override exists. + usertypes.UNSET if no such override exists. """ self.ensure_has_opt(name) value = self._values[name].get_for_pattern(pattern, fallback=False) @@ -432,7 +433,7 @@ class Config(QObject): """Get the given setting as string. If a pattern is given, get the setting for the given pattern or - configutils.UNSET. + usertypes.UNSET. """ opt = self.get_opt(name) values = self._values[name] @@ -442,14 +443,19 @@ class Config(QObject): def set_obj(self, name: str, value: Any, *, pattern: urlmatch.UrlPattern = None, - save_yaml: bool = False) -> None: + save_yaml: bool = False, + hide_userconfig: bool = False) -> None: """Set the given setting from a YAML/config.py object. If save_yaml=True is given, store the new value to YAML. + + If hide_userconfig=True is given, hide the value from + dump_userconfig(). """ opt = self.get_opt(name) self._check_yaml(opt, save_yaml) - self._set_value(opt, value, pattern=pattern) + self._set_value(opt, value, pattern=pattern, + hide_userconfig=hide_userconfig) if save_yaml: self._yaml.set_obj(name, value, pattern=pattern) @@ -519,15 +525,14 @@ class Config(QObject): Return: The changed config part as string. """ - blocks = [] + lines = [] # type: typing.List[str] for values in sorted(self, key=lambda v: v.opt.name): - if values: - blocks.append(str(values)) + lines += values.dump() - if not blocks: + if not lines: return '<Default configuration>' - return '\n'.join(blocks) + return '\n'.join(lines) class ConfigContainer: @@ -611,86 +616,3 @@ class ConfigContainer: return '{}.{}'.format(self._prefix, attr) else: return attr - - -def set_register_stylesheet(obj: QObject, *, - stylesheet: str = None, - update: bool = True) -> None: - """Set the stylesheet for an object. - - Also, register an update when the config is changed. - - Args: - obj: The object to set the stylesheet for and register. - Must have a STYLESHEET attribute if stylesheet is not given. - stylesheet: The stylesheet to use. - update: Whether to update the stylesheet on config changes. - """ - observer = StyleSheetObserver(obj, stylesheet, update) - observer.register() - - -@debugcachestats.register() -@functools.lru_cache() -def _render_stylesheet(stylesheet: str) -> str: - """Render the given stylesheet jinja template.""" - with jinja.environment.no_autoescape(): - template = jinja.environment.from_string(stylesheet) - return template.render(conf=val) - - -class StyleSheetObserver(QObject): - - """Set the stylesheet on the given object and update it on changes. - - Attributes: - _obj: The object to observe. - _stylesheet: The stylesheet template to use. - _options: The config options that the stylesheet uses. When it's not - necessary to listen for config changes, this attribute may be - None. - """ - - def __init__(self, obj: QObject, - stylesheet: Optional[str], update: bool) -> None: - super().__init__() - self._obj = obj - self._update = update - - # We only need to hang around if we are asked to update. - if update: - self.setParent(self._obj) - if stylesheet is None: - self._stylesheet = obj.STYLESHEET # type: str - else: - self._stylesheet = stylesheet - - if update: - self._options = jinja.template_config_variables( - self._stylesheet) # type: Optional[FrozenSet[str]] - else: - self._options = None - - def _get_stylesheet(self) -> str: - """Format a stylesheet based on a template. - - Return: - The formatted template as string. - """ - return _render_stylesheet(self._stylesheet) - - @pyqtSlot(str) - def _maybe_update_stylesheet(self, option: str) -> None: - """Update the stylesheet for obj if the option changed affects it.""" - assert self._options is not None - if option in self._options: - self._obj.setStyleSheet(self._get_stylesheet()) - - def register(self) -> None: - """Do a first update and listen for more.""" - qss = self._get_stylesheet() - log.config.vdebug( # type: ignore - "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss)) - self._obj.setStyleSheet(qss) - if self._update: - instance.changed.connect(self._maybe_update_stylesheet) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 84c157b28..1814c3a5a 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -178,6 +178,19 @@ qt.force_platform: This sets the `QT_QPA_PLATFORM` environment variable and is useful to force using the XCB plugin when running QtWebEngine on Wayland. +qt.force_platformtheme: + type: + name: String + none_ok: true + default: null + restart: true + desc: >- + Force a Qt platformtheme to use. + + This sets the `QT_QPA_PLATFORMTHEME` environment variable which controls + dialogs like the filepicker. By default, Qt determines the platform theme + based on the desktop environment. + qt.process_model: type: name: String @@ -395,6 +408,15 @@ content.frame_flattening: This will flatten all the frames to become one scrollable page. +content.site_specific_quirks: + default: true + restart: true + type: Bool + desc: 'Enable quirks (such as faked user agent headers) needed to get + specific sites to work properly.' + +# emacs: ' + content.geolocation: default: ask type: BoolAsk @@ -469,10 +491,20 @@ content.headers.referer: No restart is needed with QtWebKit. content.headers.user_agent: - default: null + default: 'Mozilla/5.0 ({os_info}) + AppleWebKit/{webkit_version} (KHTML, like Gecko) + {qt_key}/{qt_version} {upstream_browser_key}/{upstream_browser_version} + Safari/{webkit_version}' type: - name: String - none_ok: true + name: FormatString + fields: + - os_info + - webkit_version + - qt_key + - qt_version + - upstream_browser_key + - upstream_browser_version + - qutebrowser_version completions: # To update the following list of user agents, run the script # 'ua_fetch.py' @@ -484,12 +516,23 @@ content.headers.user_agent: - - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" - Chrome 76 Linux - - - "" - - Use default QtWebKit/QtWebEngine User-Agent - supports_pattern: true - desc: >- - User agent to send. Unset to send the default. + desc: | + User agent to send. + + The following placeholders are defined: + + * `{os_info}`: Something like "X11; Linux x86_64". + * `{webkit_version}`: The underlying WebKit version (set to a fixed value + with QtWebEngine). + * `{qt_key}`: "Qt" for QtWebKit, "QtWebEngine" for QtWebEngine. + * `{qt_version}`: The underlying Qt version. + * `{upstream_browser_key}`: "Version" for QtWebKit, "Chrome" for QtWebEngine. + * `{upstream_browser_version}`: The corresponding Safari/Chrome version. + * `{qutebrowser_version}`: The currently running qutebrowser version. + + The default value is equal to the unchanged user agent of + QtWebKit/QtWebEngine. Note that the value read from JavaScript is always the global value. @@ -1008,7 +1051,7 @@ editor.command: name: ShellCommand placeholder: true default: ["gvim", "-f", "{file}", "-c", "normal {line}G{column0}l"] - desc: >- + desc: | Editor (and arguments) to use for the `open-editor` command. The following placeholders are defined: @@ -2041,6 +2084,12 @@ colors.completion.scrollbar.bg: desc: Color of the scrollbar in the completion view. colors.contextmenu.bg: + renamed: colors.contextmenu.menu.bg + +colors.contextmenu.fg: + renamed: colors.contextmenu.menu.fg + +colors.contextmenu.menu.bg: type: name: QssColor none_ok: true @@ -2050,7 +2099,7 @@ colors.contextmenu.bg: If set to null, the Qt default is used. -colors.contextmenu.fg: +colors.contextmenu.menu.fg: type: name: QssColor none_ok: true @@ -2060,6 +2109,26 @@ colors.contextmenu.fg: If set to null, the Qt default is used. +colors.contextmenu.selected.bg: + type: + name: QssColor + none_ok: true + default: null + desc: >- + Background color of the context menu's selected item. + + If set to null, the Qt default is used. + +colors.contextmenu.selected.fg: + type: + name: QssColor + none_ok: true + default: null + desc: >- + Foreground color of the context menu's selected item. + + If set to null, the Qt default is used. + colors.downloads.bar.bg: default: black type: QssColor diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index d83ca403b..53b6689f8 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -22,7 +22,7 @@ import typing import attr -from qutebrowser.utils import jinja, usertypes, log +from qutebrowser.utils import usertypes, log class Error(Exception): @@ -155,6 +155,7 @@ class ConfigFileErrors(Error): def to_html(self) -> str: """Get the error texts as a HTML snippet.""" + from qutebrowser.utils import jinja # circular import template = jinja.environment.from_string(""" Errors occurred while reading {{ basename }}: diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index ebfec1354..2e3e2f632 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -31,7 +31,7 @@ import typing import re import yaml -from PyQt5.QtCore import pyqtSignal, QObject, QSettings, qVersion +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSettings, qVersion import qutebrowser from qutebrowser.config import configexc, config, configdata, configutils @@ -46,6 +46,9 @@ if typing.TYPE_CHECKING: state = typing.cast('StateConfig', None) +_SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]] + + class StateConfig(configparser.ConfigParser): """The "state" file saving various application state.""" @@ -103,8 +106,6 @@ class YamlConfig(QObject): VERSION = 2 changed = pyqtSignal() - _SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]] - def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._filename = os.path.join(standarddir.config(auto=True), @@ -128,6 +129,7 @@ class YamlConfig(QObject): """Iterate over configutils.Values items.""" yield from self._values.values() + @pyqtSlot() def _mark_changed(self) -> None: """Mark the YAML config as changed.""" self._dirty = True @@ -138,7 +140,7 @@ class YamlConfig(QObject): if not self._dirty: return - settings = {} # type: YamlConfig._SettingsType + settings = {} # type: _SettingsType for name, values in sorted(self._values.items()): if not values: continue @@ -213,7 +215,10 @@ class YamlConfig(QObject): settings = self._load_settings_object(yaml_data) self._dirty = False - settings = self._handle_migrations(settings) + migrations = YamlMigrations(settings, parent=self) + migrations.changed.connect(self._mark_changed) + migrations.migrate() + self._validate(settings) self._build_values(settings) @@ -262,89 +267,6 @@ class YamlConfig(QObject): if errors: raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def _migrate_bool(self, settings: _SettingsType, name: str, - true_value: str, false_value: str) -> None: - """Migrate a boolean in the settings.""" - if name in settings: - for scope, val in settings[name].items(): - if isinstance(val, bool): - settings[name][scope] = true_value if val else false_value - self._mark_changed() - - def _migrate_string_value(self, settings: _SettingsType, name: str, - source: str, target: str) -> None: - if name in settings: - for scope, val in settings[name].items(): - if isinstance(val, str): - new_val = re.sub(source, target, val) - if new_val != val: - settings[name][scope] = new_val - self._mark_changed() - - def _handle_migrations(self, settings: _SettingsType) -> '_SettingsType': - """Migrate older configs to the newest format.""" - # Simple renamed/deleted options - for name in list(settings): - if name in configdata.MIGRATIONS.renamed: - new_name = configdata.MIGRATIONS.renamed[name] - log.config.debug("Renaming {} to {}".format(name, new_name)) - settings[new_name] = settings[name] - del settings[name] - self._mark_changed() - elif name in configdata.MIGRATIONS.deleted: - log.config.debug("Removing {}".format(name)) - del settings[name] - self._mark_changed() - - # tabs.persist_mode_on_change got merged into tabs.mode_on_change - old = 'tabs.persist_mode_on_change' - new = 'tabs.mode_on_change' - if old in settings: - settings[new] = {} - for scope, val in settings[old].items(): - if val: - settings[new][scope] = 'persist' - else: - settings[new][scope] = 'normal' - - del settings[old] - self._mark_changed() - - # bindings.default can't be set in autoconfig.yml anymore, so ignore - # old values. - if 'bindings.default' in settings: - del settings['bindings.default'] - self._mark_changed() - - # content.webrtc_public_interfaces_only got merged into - # content.webrtc_ip_handling_policy. - old = 'content.webrtc_public_interfaces_only' - new = 'content.webrtc_ip_handling_policy' - if old in settings: - settings[new] = {} - for scope, val in settings[old].items(): - if val: - settings[new][scope] = 'default-public-interface-only' - else: - settings[new][scope] = 'all-interfaces' - - del settings[old] - self._mark_changed() - - self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never') - self._migrate_bool(settings, 'scrolling.bar', - 'always', 'when-searching') - self._migrate_bool(settings, 'qt.force_software_rendering', - 'software-opengl', 'none') - - for s in ['tabs.title.format', - 'tabs.title.format_pinned', - 'window.title_format']: - self._migrate_string_value( - settings, s, r'(?<!{)\{title\}(?!})', r'{current_title}') - - return settings - def _validate(self, settings: _SettingsType) -> None: """Make sure all settings exist.""" unknown = [] @@ -377,6 +299,124 @@ class YamlConfig(QObject): self._mark_changed() +class YamlMigrations(QObject): + + """Automated migrations for autoconfig.yml.""" + + changed = pyqtSignal() + + def __init__(self, settings: _SettingsType, + parent: QObject = None) -> None: + super().__init__(parent) + self._settings = settings + + def migrate(self) -> None: + """Migrate older configs to the newest format.""" + self._migrate_configdata() + self._migrate_bindings_default() + + self._migrate_bool('tabs.favicons.show', 'always', 'never') + self._migrate_bool('scrolling.bar', 'always', 'when-searching') + self._migrate_bool('qt.force_software_rendering', + 'software-opengl', 'none') + self._migrate_renamed_bool( + old_name='content.webrtc_public_interfaces_only', + new_name='content.webrtc_ip_handling_policy', + true_value='default-public-interface-only', + false_value='all-interfaces') + self._migrate_renamed_bool( + old_name='tabs.persist_mode_on_change', + new_name='tabs.mode_on_change', + true_value='persist', + false_value='normal') + + for setting in ['tabs.title.format', + 'tabs.title.format_pinned', + 'window.title_format']: + self._migrate_string_value(setting, + r'(?<!{)\{title\}(?!})', + r'{current_title}') + + # content.headers.user_agent can't be empty to get the default anymore. + setting = 'content.headers.user_agent' + self._migrate_none(setting, configdata.DATA[setting].default) + + def _migrate_configdata(self) -> None: + """Migrate simple renamed/deleted options.""" + for name in list(self._settings): + if name in configdata.MIGRATIONS.renamed: + new_name = configdata.MIGRATIONS.renamed[name] + log.config.debug("Renaming {} to {}".format(name, new_name)) + self._settings[new_name] = self._settings[name] + del self._settings[name] + self.changed.emit() + elif name in configdata.MIGRATIONS.deleted: + log.config.debug("Removing {}".format(name)) + del self._settings[name] + self.changed.emit() + + def _migrate_bindings_default(self) -> None: + """bindings.default can't be set in autoconfig.yml anymore. + + => Ignore old values. + """ + if 'bindings.default' not in self._settings: + return + + del self._settings['bindings.default'] + self.changed.emit() + + def _migrate_bool(self, name: str, + true_value: str, + false_value: str) -> None: + if name not in self._settings: + return + + for scope, val in self._settings[name].items(): + if isinstance(val, bool): + new_value = true_value if val else false_value + self._settings[name][scope] = new_value + self.changed.emit() + + def _migrate_renamed_bool(self, old_name: str, + new_name: str, + true_value: str, + false_value: str) -> None: + if old_name not in self._settings: + return + + self._settings[new_name] = {} + + for scope, val in self._settings[old_name].items(): + new_value = true_value if val else false_value + self._settings[new_name][scope] = new_value + + del self._settings[old_name] + self.changed.emit() + + def _migrate_none(self, name: str, value: str) -> None: + if name not in self._settings: + return + + for scope, val in self._settings[name].items(): + if val is None: + self._settings[name][scope] = value + self.changed.emit() + + def _migrate_string_value(self, name: str, + source: str, + target: str) -> None: + if name not in self._settings: + return + + for scope, val in self._settings[name].items(): + if isinstance(val, str): + new_val = re.sub(source, target, val) + if new_val != val: + self._settings[name][scope] = new_val + self.changed.emit() + + class ConfigAPI: """Object which gets passed to config.py as "config" object. diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 21f7fa054..2924efeae 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -28,7 +28,7 @@ from PyQt5.QtWidgets import QMessageBox from qutebrowser.api import config as configapi from qutebrowser.config import (config, configdata, configfiles, configtypes, - configexc, configcommands) + configexc, configcommands, stylesheet) from qutebrowser.utils import (objreg, usertypes, log, standarddir, message, qtutils) from qutebrowser.config import configcache @@ -88,6 +88,8 @@ def early_init(args: argparse.Namespace) -> None: configtypes.Font.monospace_fonts = config.val.fonts.monospace config.instance.changed.connect(_update_monospace_fonts) + stylesheet.init() + _init_envvars() @@ -104,6 +106,8 @@ def _init_envvars() -> None: if config.val.qt.force_platform is not None: os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform + if config.val.qt.force_platformtheme is not None: + os.environ['QT_QPA_PLATFORMTHEME'] = config.val.qt.force_platformtheme if config.val.window.hide_decoration: os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index d48c02250..475879de0 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -61,7 +61,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.misc import objects, debugcachestats -from qutebrowser.config import configexc, configutils +from qutebrowser.config import configexc from qutebrowser.utils import (standarddir, utils, qtutils, urlutils, urlmatch, usertypes) from qutebrowser.keyinput import keyutils @@ -80,8 +80,8 @@ BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, _Completions = typing.Optional[typing.Iterable[typing.Tuple[str, str]]] -_StrUnset = typing.Union[str, configutils.Unset] -_StrUnsetNone = typing.Union[None, str, configutils.Unset] +_StrUnset = typing.Union[str, usertypes.Unset] +_StrUnsetNone = typing.Union[None, str, usertypes.Unset] class ValidValues: @@ -168,7 +168,7 @@ class BaseType: value: The value to check. pytype: A Python type to check the value against. """ - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return if (value is None or (pytype == list and value == []) or @@ -342,7 +342,7 @@ class MappingType(BaseType): def to_py(self, value: typing.Any) -> typing.Any: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -408,7 +408,7 @@ class String(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -450,7 +450,7 @@ class UniqueCharString(String): def to_py(self, value: _StrUnset) -> _StrUnsetNone: py_value = super().to_py(value) - if isinstance(py_value, configutils.Unset): + if isinstance(py_value, usertypes.Unset): return py_value elif not py_value: return None @@ -510,10 +510,10 @@ class List(BaseType): def to_py( self, - value: typing.Union[typing.List, configutils.Unset] - ) -> typing.Union[typing.List, configutils.Unset]: + value: typing.Union[typing.List, usertypes.Unset] + ) -> typing.Union[typing.List, usertypes.Unset]: self._basic_py_validation(value, list) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return [] @@ -601,7 +601,7 @@ class ListOrValue(BaseType): return value def to_py(self, value: typing.Any) -> typing.Any: - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value try: @@ -652,10 +652,10 @@ class FlagList(List): def to_py( self, - value: typing.Union[configutils.Unset, typing.List], - ) -> typing.Union[configutils.Unset, typing.List]: + value: typing.Union[usertypes.Unset, typing.List], + ) -> typing.Union[usertypes.Unset, typing.List]: vals = super().to_py(value) - if not isinstance(vals, configutils.Unset): + if not isinstance(vals, usertypes.Unset): self._check_duplicates(vals) return vals @@ -866,10 +866,10 @@ class Perc(_Numeric): def to_py( self, - value: typing.Union[None, float, int, str, configutils.Unset] - ) -> typing.Union[None, float, int, configutils.Unset]: + value: typing.Union[None, float, int, str, usertypes.Unset] + ) -> typing.Union[None, float, int, usertypes.Unset]: self._basic_py_validation(value, (float, int, str)) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1062,10 +1062,10 @@ class QtColor(BaseType): except ValueError: raise configexc.ValidationError(val, "must be a valid color value") - def to_py(self, value: _StrUnset) -> typing.Union[configutils.Unset, + def to_py(self, value: _StrUnset) -> typing.Union[usertypes.Unset, None, QColor]: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1123,7 +1123,7 @@ class QssColor(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1170,7 +1170,7 @@ class Font(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1191,7 +1191,7 @@ class FontFamily(Font): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1215,10 +1215,10 @@ class QtFont(Font): __doc__ = Font.__doc__ # for src2asciidoc.py - def to_py(self, value: _StrUnset) -> typing.Union[configutils.Unset, + def to_py(self, value: _StrUnset) -> typing.Union[usertypes.Unset, None, QFont]: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1333,11 +1333,11 @@ class Regex(BaseType): def to_py( self, - value: typing.Union[str, typing.Pattern[str], configutils.Unset] - ) -> typing.Union[configutils.Unset, None, typing.Pattern[str]]: + value: typing.Union[str, typing.Pattern[str], usertypes.Unset] + ) -> typing.Union[usertypes.Unset, None, typing.Pattern[str]]: """Get a compiled regex from either a string or a regex object.""" self._basic_py_validation(value, (str, self._regex_type)) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1425,10 +1425,10 @@ class Dict(BaseType): def to_py( self, - value: typing.Union[typing.Dict, configutils.Unset, None] - ) -> typing.Union[typing.Dict, configutils.Unset]: + value: typing.Union[typing.Dict, usertypes.Unset, None] + ) -> typing.Union[typing.Dict, usertypes.Unset]: self._basic_py_validation(value, dict) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return self._fill_fixed_keys({}) @@ -1477,7 +1477,7 @@ class File(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1509,7 +1509,7 @@ class Directory(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1530,16 +1530,23 @@ class Directory(BaseType): class FormatString(BaseType): - """A string with placeholders.""" + """A string with placeholders. + + Attributes: + fields: Which replacements are allowed in the format string. + completions: completions to be used, or None + """ def __init__(self, fields: typing.Iterable[str], - none_ok: bool = False) -> None: + none_ok: bool = False, + completions: _Completions = None) -> None: super().__init__(none_ok) self.fields = fields + self._completions = completions def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1554,6 +1561,12 @@ class FormatString(BaseType): return value + def complete(self) -> _Completions: + if self._completions is not None: + return self._completions + else: + return super().complete() + def __repr__(self) -> str: return utils.get_repr(self, none_ok=self.none_ok, fields=self.fields) @@ -1577,10 +1590,10 @@ class ShellCommand(List): def to_py( self, - value: typing.Union[typing.List, configutils.Unset], - ) -> typing.Union[typing.List, configutils.Unset]: + value: typing.Union[typing.List, usertypes.Unset], + ) -> typing.Union[typing.List, usertypes.Unset]: py_value = super().to_py(value) - if isinstance(py_value, configutils.Unset): + if isinstance(py_value, usertypes.Unset): return py_value elif not py_value: return [] @@ -1611,9 +1624,9 @@ class Proxy(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[configutils.Unset, None, QNetworkProxy, _SystemProxy]: + ) -> typing.Union[usertypes.Unset, None, QNetworkProxy, _SystemProxy]: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1653,7 +1666,7 @@ class SearchEngineUrl(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1683,7 +1696,7 @@ class FuzzyUrl(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1719,10 +1732,10 @@ class Padding(Dict): def to_py( # type: ignore self, - value: typing.Union[configutils.Unset, typing.Dict, None], - ) -> typing.Union[configutils.Unset, PaddingValues]: + value: typing.Union[usertypes.Unset, typing.Dict, None], + ) -> typing.Union[usertypes.Unset, PaddingValues]: d = super().to_py(value) - if isinstance(d, configutils.Unset): + if isinstance(d, usertypes.Unset): return d return PaddingValues(**d) @@ -1734,7 +1747,7 @@ class Encoding(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1794,9 +1807,9 @@ class Url(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[configutils.Unset, None, QUrl]: + ) -> typing.Union[usertypes.Unset, None, QUrl]: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1814,7 +1827,7 @@ class SessionName(BaseType): def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1864,10 +1877,10 @@ class ConfirmQuit(FlagList): def to_py( self, - value: typing.Union[configutils.Unset, typing.List], - ) -> typing.Union[typing.List, configutils.Unset]: + value: typing.Union[usertypes.Unset, typing.List], + ) -> typing.Union[typing.List, usertypes.Unset]: values = super().to_py(value) - if isinstance(values, configutils.Unset): + if isinstance(values, usertypes.Unset): return values elif not values: return [] @@ -1908,9 +1921,9 @@ class Key(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[configutils.Unset, None, keyutils.KeySequence]: + ) -> typing.Union[usertypes.Unset, None, keyutils.KeySequence]: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None @@ -1932,9 +1945,9 @@ class UrlPattern(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[configutils.Unset, None, urlmatch.UrlPattern]: + ) -> typing.Union[usertypes.Unset, None, urlmatch.UrlPattern]: self._basic_py_validation(value, str) - if isinstance(value, configutils.Unset): + if isinstance(value, usertypes.Unset): return value elif not value: return None diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index 8997aef36..1a7f612cb 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -28,24 +28,20 @@ import operator from PyQt5.QtCore import QUrl -from qutebrowser.utils import utils, urlmatch, urlutils +from qutebrowser.utils import utils, urlmatch, usertypes from qutebrowser.config import configexc if typing.TYPE_CHECKING: from qutebrowser.config import configdata -class Unset: +def _widened_hostnames(hostname: str) -> typing.Iterable[str]: + """A generator for widening string hostnames. - """Sentinel object.""" - - __slots__ = () - - def __repr__(self) -> str: - return '<UNSET>' - - -UNSET = Unset() + Ex: a.c.foo -> [a.c.foo, c.foo, foo]""" + while hostname: + yield hostname + hostname = hostname.partition(".")[-1] class ScopedValue: @@ -55,18 +51,22 @@ class ScopedValue: Attributes: value: The value itself. pattern: The UrlPattern for the value, or None for global values. + hide_userconfig: Hide this customization from config.dump_userconfig(). """ id_gen = itertools.count(0) def __init__(self, value: typing.Any, - pattern: typing.Optional[urlmatch.UrlPattern]) -> None: + pattern: typing.Optional[urlmatch.UrlPattern], + hide_userconfig: bool = False) -> None: self.value = value self.pattern = pattern + self.hide_userconfig = hide_userconfig self.pattern_id = next(ScopedValue.id_gen) def __repr__(self) -> str: return utils.get_repr(self, value=self.value, pattern=self.pattern, + hide_userconfig=self.hide_userconfig, pattern_id=self.pattern_id) @@ -102,8 +102,8 @@ class Values: self._domain_map = collections.defaultdict(set) \ # type: typing.Dict[typing.Optional[str], typing.Set[ScopedValue]] - for v in values: - self.add(value=v.value, pattern=v.pattern) + for scoped in values: + self._add_scoped(scoped) def __repr__(self) -> str: return utils.get_repr(self, opt=self.opt, @@ -112,18 +112,31 @@ class Values: def __str__(self) -> str: """Get the values as human-readable string.""" - if not self: - return '{}: <unchanged>'.format(self.opt.name) + lines = self.dump(include_hidden=True) + if lines: + return '\n'.join(lines) + return '{}: <unchanged>'.format(self.opt.name) + + def dump(self, include_hidden: bool = False) -> typing.Sequence[str]: + """Dump all customizations for this value. + Arguments: + include_hidden: Also show values with hide_userconfig=True. + """ lines = [] + for scoped in self._vmap.values(): + if scoped.hide_userconfig and not include_hidden: + continue + str_value = self.opt.typ.to_str(scoped.value) if scoped.pattern is None: lines.append('{} = {}'.format(self.opt.name, str_value)) else: lines.append('{}: {} = {}'.format( scoped.pattern, self.opt.name, str_value)) - return '\n'.join(lines) + + return lines def __iter__(self) -> typing.Iterator['ScopedValue']: """Yield ScopedValue elements. @@ -144,14 +157,24 @@ class Values: raise configexc.NoPatternError(self.opt.name) def add(self, value: typing.Any, - pattern: urlmatch.UrlPattern = None) -> None: - """Add a value with the given pattern to the list of values.""" - self._check_pattern_support(pattern) - self.remove(pattern) - scoped = ScopedValue(value, pattern) - self._vmap[pattern] = scoped + pattern: urlmatch.UrlPattern = None, *, + hide_userconfig: bool = False) -> None: + """Add a value with the given pattern to the list of values. - host = pattern.host if pattern else None + If hide_userconfig is given, the value is hidden from + config.dump_userconfig() and thus qute://configdiff. + """ + scoped = ScopedValue(value, pattern, hide_userconfig=hide_userconfig) + self._add_scoped(scoped) + + def _add_scoped(self, scoped: ScopedValue) -> None: + """Add an existing ScopedValue object.""" + self._check_pattern_support(scoped.pattern) + self.remove(scoped.pattern) + + self._vmap[scoped.pattern] = scoped + + host = scoped.pattern.host if scoped.pattern else None self._domain_map[host].add(scoped) def remove(self, pattern: urlmatch.UrlPattern = None) -> bool: @@ -186,7 +209,7 @@ class Values: if fallback: return self.opt.default else: - return UNSET + return usertypes.UNSET def get_for_url(self, url: QUrl = None, *, fallback: bool = True) -> typing.Any: @@ -195,14 +218,14 @@ class Values: This first tries to find a value matching the URL (if given). If there's no match: With fallback=True, the global/default setting is returned. - With fallback=False, UNSET is returned. + With fallback=False, usertypes.UNSET is returned. """ self._check_pattern_support(url) if url is None: return self._get_fallback(fallback) candidates = [] # type: typing.List[ScopedValue] - widened_hosts = urlutils.widened_hostnames(url.host()) + widened_hosts = _widened_hostnames(url.host()) # We must check the 'None' key as well, in case any patterns that # did not have a domain match. for host in itertools.chain(widened_hosts, [None]): @@ -216,7 +239,7 @@ class Values: return scoped.value if not fallback: - return UNSET + return usertypes.UNSET return self._get_fallback(fallback) @@ -229,7 +252,7 @@ class Values: If there's no match: With fallback=True, the global/default setting is returned. - With fallback=False, UNSET is returned. + With fallback=False, usertypes.UNSET is returned. """ self._check_pattern_support(pattern) if pattern is not None: @@ -237,6 +260,6 @@ class Values: return self._vmap[pattern].value if not fallback: - return UNSET + return usertypes.UNSET return self._get_fallback(fallback) diff --git a/qutebrowser/config/stylesheet.py b/qutebrowser/config/stylesheet.py new file mode 100644 index 000000000..27432d01b --- /dev/null +++ b/qutebrowser/config/stylesheet.py @@ -0,0 +1,116 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2019 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +"""Handling of Qt qss stylesheets.""" + +import functools +from typing import Optional, FrozenSet + +from PyQt5.QtCore import pyqtSlot, QObject + +from qutebrowser.config import config +from qutebrowser.misc import debugcachestats +from qutebrowser.utils import jinja, log + + +def set_register(obj: QObject, + stylesheet: str = None, *, + update: bool = True) -> None: + """Set the stylesheet for an object. + + Also, register an update when the config is changed. + + Args: + obj: The object to set the stylesheet for and register. + Must have a STYLESHEET attribute if stylesheet is not given. + stylesheet: The stylesheet to use. + update: Whether to update the stylesheet on config changes. + """ + observer = _StyleSheetObserver(obj, stylesheet, update) + observer.register() + + +@debugcachestats.register() +@functools.lru_cache() +def _render_stylesheet(stylesheet: str) -> str: + """Render the given stylesheet jinja template.""" + with jinja.environment.no_autoescape(): + template = jinja.environment.from_string(stylesheet) + return template.render(conf=config.val) + + +def init() -> None: + config.instance.changed.connect(_render_stylesheet.cache_clear) + + +class _StyleSheetObserver(QObject): + + """Set the stylesheet on the given object and update it on changes. + + Attributes: + _obj: The object to observe. + _stylesheet: The stylesheet template to use. + _options: The config options that the stylesheet uses. When it's not + necessary to listen for config changes, this attribute may be + None. + """ + + def __init__(self, obj: QObject, + stylesheet: Optional[str], update: bool) -> None: + super().__init__() + self._obj = obj + self._update = update + + # We only need to hang around if we are asked to update. + if update: + self.setParent(self._obj) + if stylesheet is None: + self._stylesheet = obj.STYLESHEET # type: str + else: + self._stylesheet = stylesheet + + if update: + self._options = jinja.template_config_variables( + self._stylesheet) # type: Optional[FrozenSet[str]] + else: + self._options = None + + def _get_stylesheet(self) -> str: + """Format a stylesheet based on a template. + + Return: + The formatted template as string. + """ + return _render_stylesheet(self._stylesheet) + + @pyqtSlot(str) + def _maybe_update_stylesheet(self, option: str) -> None: + """Update the stylesheet for obj if the option changed affects it.""" + assert self._options is not None + if option in self._options: + self._obj.setStyleSheet(self._get_stylesheet()) + + def register(self) -> None: + """Do a first update and listen for more.""" + qss = self._get_stylesheet() + log.config.vdebug( # type: ignore + "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss)) + self._obj.setStyleSheet(qss) + if self._update: + config.instance.changed.connect(self._maybe_update_stylesheet) diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index db8f77387..2f78b561e 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -19,19 +19,65 @@ """Bridge from QWeb(Engine)Settings to our own settings.""" +import re import typing import argparse +import functools -from PyQt5.QtCore import QUrl, pyqtSlot +import attr +from PyQt5.QtCore import QUrl, pyqtSlot, qVersion from PyQt5.QtGui import QFont -from qutebrowser.config import config, configutils +import qutebrowser +from qutebrowser.config import config from qutebrowser.utils import log, usertypes, urlmatch, qtutils -from qutebrowser.misc import objects +from qutebrowser.misc import objects, debugcachestats UNSET = object() +@attr.s +class UserAgent: + + """A parsed user agent.""" + + os_info = attr.ib() # type: str + webkit_version = attr.ib() # type: str + upstream_browser_key = attr.ib() # type: str + upstream_browser_version = attr.ib() # type: str + qt_key = attr.ib() # type: str + + @classmethod + def parse(cls, ua: str) -> 'UserAgent': + """Parse a user agent string into its components.""" + comment_matches = re.finditer(r'\(([^)]*)\)', ua) + os_info = list(comment_matches)[0].group(1) + + version_matches = re.finditer(r'(\S+)/(\S+)', ua) + versions = {} + for match in version_matches: + versions[match.group(1)] = match.group(2) + + webkit_version = versions['AppleWebKit'] + + if 'Chrome' in versions: + upstream_browser_key = 'Chrome' + qt_key = 'QtWebEngine' + elif 'Version' in versions: + upstream_browser_key = 'Version' + qt_key = 'Qt' + else: + raise ValueError("Invalid upstream browser key: {}".format(ua)) + + upstream_browser_version = versions[upstream_browser_key] + + return cls(os_info=os_info, + webkit_version=webkit_version, + upstream_browser_key=upstream_browser_key, + upstream_browser_version=upstream_browser_version, + qt_key=qt_key) + + class AttributeInfo: """Info about a settings attribute.""" @@ -60,7 +106,7 @@ class AbstractSettings: def set_attribute(self, name: str, value: typing.Any) -> bool: """Set the given QWebSettings/QWebEngineSettings attribute. - If the value is configutils.UNSET, the value is reset instead. + If the value is usertypes.UNSET, the value is reset instead. Return: True if there was a change, False otherwise. @@ -69,7 +115,7 @@ class AbstractSettings: info = self._ATTRIBUTES[name] for attribute in info.attributes: - if value is configutils.UNSET: + if value is usertypes.UNSET: self._settings.resetAttribute(attribute) new_value = self.test_attribute(name) else: @@ -93,7 +139,7 @@ class AbstractSettings: Return: True if there was a change, False otherwise. """ - assert value is not configutils.UNSET # type: ignore + assert value is not usertypes.UNSET # type: ignore family = self._FONT_SIZES[name] old_value = self._settings.fontSize(family) self._settings.setFontSize(family, value) @@ -108,7 +154,7 @@ class AbstractSettings: Return: True if there was a change, False otherwise. """ - assert value is not configutils.UNSET # type: ignore + assert value is not usertypes.UNSET # type: ignore family = self._FONT_FAMILIES[name] if value is None: font = QFont() @@ -126,7 +172,7 @@ class AbstractSettings: Return: True if there was a change, False otherwise. """ - assert encoding is not configutils.UNSET # type: ignore + assert encoding is not usertypes.UNSET # type: ignore old_value = self._settings.defaultTextEncoding() self._settings.setDefaultTextEncoding(encoding) return old_value != encoding @@ -183,6 +229,34 @@ class AbstractSettings: self.update_setting(setting) +@debugcachestats.register(name='user agent cache') +@functools.lru_cache() +def _format_user_agent(template: str, backend: usertypes.Backend) -> str: + if backend == usertypes.Backend.QtWebEngine: + from qutebrowser.browser.webengine import webenginesettings + parsed = webenginesettings.parsed_user_agent + else: + from qutebrowser.browser.webkit import webkitsettings + parsed = webkitsettings.parsed_user_agent + + assert parsed is not None + + return template.format( + os_info=parsed.os_info, + webkit_version=parsed.webkit_version, + qt_key=parsed.qt_key, + qt_version=qVersion(), + upstream_browser_key=parsed.upstream_browser_key, + upstream_browser_version=parsed.upstream_browser_version, + qutebrowser_version=qutebrowser.__version__, + ) + + +def user_agent(url: QUrl = None) -> str: + template = config.instance.get('content.headers.user_agent', url=url) + return _format_user_agent(template=template, backend=objects.backend) + + def init(args: argparse.Namespace) -> None: """Initialize all QWeb(Engine)Settings.""" if objects.backend == usertypes.Backend.QtWebEngine: @@ -193,9 +267,10 @@ def init(args: argparse.Namespace) -> None: webkitsettings.init(args) # Make sure special URLs always get JS support - for pattern in ['file://*', 'chrome://*/*', 'qute://*/*']: + for pattern in ['chrome://*/*', 'qute://*/*']: config.instance.set_obj('content.javascript.enabled', True, - pattern=urlmatch.UrlPattern(pattern)) + pattern=urlmatch.UrlPattern(pattern), + hide_userconfig=True) @pyqtSlot() diff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py index a72fe6cfa..58bd3f2bc 100644 --- a/qutebrowser/extensions/interceptors.py +++ b/qutebrowser/extensions/interceptors.py @@ -52,6 +52,9 @@ class ResourceType(enum.Enum): service_worker = 15 csp_report = 16 plugin_resource = 17 + # 18 is "preload", deprecated in Chromium + preload_main_frame = 19 + preload_sub_frame = 20 unknown = 255 diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index ac5df12e6..c2dea4dc7 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from qutebrowser.commands import runners from qutebrowser.api import cmdutils -from qutebrowser.config import config, configfiles +from qutebrowser.config import config, configfiles, stylesheet from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, jinja, debug) from qutebrowser.mainwindow import messageview, prompt @@ -165,11 +165,20 @@ class MainWindow(QWidget): {% if conf.fonts.contextmenu %} font: {{ conf.fonts.contextmenu }}; {% endif %} - {% if conf.colors.contextmenu.bg %} - background-color: {{ conf.colors.contextmenu.bg }}; + {% if conf.colors.contextmenu.menu.bg %} + background-color: {{ conf.colors.contextmenu.menu.bg }}; {% endif %} - {% if conf.colors.contextmenu.fg %} - color: {{ conf.colors.contextmenu.fg }}; + {% if conf.colors.contextmenu.menu.fg %} + color: {{ conf.colors.contextmenu.menu.fg }}; + {% endif %} + } + + QMenu::item:selected { + {% if conf.colors.contextmenu.selected.bg %} + background-color: {{ conf.colors.contextmenu.selected.bg }}; + {% endif %} + {% if conf.colors.contextmenu.selected.fg %} + color: {{ conf.colors.contextmenu.selected.fg }}; {% endif %} } """ @@ -269,7 +278,7 @@ class MainWindow(QWidget): self._set_decoration(config.val.window.hide_decoration) self.state_before_fullscreen = self.windowState() - config.set_register_stylesheet(self) + stylesheet.set_register(self) def _init_geometry(self, geometry): """Initialize the window geometry or load it from disk.""" diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index ea14265aa..3660d3529 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -24,7 +24,7 @@ import typing from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy -from qutebrowser.config import config +from qutebrowser.config import config, stylesheet from qutebrowser.utils import usertypes @@ -36,19 +36,19 @@ class Message(QLabel): super().__init__(text, parent) self.replace = replace self.setAttribute(Qt.WA_StyledBackground, True) - stylesheet = """ + qss = """ padding-top: 2px; padding-bottom: 2px; """ if level == usertypes.MessageLevel.error: - stylesheet += """ + qss += """ 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 += """ + qss += """ background-color: {{ conf.colors.messages.warning.bg }}; color: {{ conf.colors.messages.warning.fg }}; font: {{ conf.fonts.messages.warning }}; @@ -56,7 +56,7 @@ class Message(QLabel): 1px solid {{ conf.colors.messages.warning.border }}; """ elif level == usertypes.MessageLevel.info: - stylesheet += """ + qss += """ background-color: {{ conf.colors.messages.info.bg }}; color: {{ conf.colors.messages.info.fg }}; font: {{ conf.fonts.messages.info }}; @@ -66,8 +66,7 @@ class Message(QLabel): raise ValueError("Invalid level {!r}".format(level)) # We don't bother with set_register_stylesheet here as it's short-lived # anyways. - config.set_register_stylesheet(self, stylesheet=stylesheet, - update=False) + stylesheet.set_register(self, qss, update=False) class MessageView(QWidget): diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 6f7d1b9f7..ba6ff2f1b 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, QSpacerItem) from qutebrowser.browser import downloads -from qutebrowser.config import config, configtypes, configexc +from qutebrowser.config import config, configtypes, configexc, stylesheet from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message from qutebrowser.keyinput import modeman from qutebrowser.api import cmdutils @@ -292,7 +292,7 @@ class PromptContainer(QWidget): self.setObjectName('PromptContainer') self.setAttribute(Qt.WA_StyledBackground, True) - config.set_register_stylesheet(self) + stylesheet.set_register(self) message.global_bridge.prompt_done.connect(self._on_prompt_done) prompt_queue.show_prompts.connect(self._on_show_prompts) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 463e0c151..dd50024b3 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy from qutebrowser.browser import browsertab -from qutebrowser.config import config +from qutebrowser.config import config, stylesheet from qutebrowser.keyinput import modeman from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.mainwindow.statusbar import (backforward, command, progress, @@ -97,7 +97,7 @@ def _generate_stylesheet(): ('passthrough', 'statusbar.passthrough'), ('private-command', 'statusbar.command.private'), ] - stylesheet = """ + qss = """ QWidget#StatusBar, QWidget#StatusBar QLabel, QWidget#StatusBar QLineEdit { @@ -110,7 +110,7 @@ def _generate_stylesheet(): } """ for flag, option in flags: - stylesheet += """ + qss += """ QWidget#StatusBar[color_flags~="%s"], QWidget#StatusBar[color_flags~="%s"] QLabel, QWidget#StatusBar[color_flags~="%s"] QLineEdit { @@ -122,7 +122,7 @@ def _generate_stylesheet(): } """ % (flag, flag, flag, # noqa: S001 option + '.fg', flag, option + '.bg') - return stylesheet + return qss class StatusBar(QWidget): @@ -158,7 +158,7 @@ class StatusBar(QWidget): super().__init__(parent) self.setObjectName(self.__class__.__name__) self.setAttribute(Qt.WA_StyledBackground) - config.set_register_stylesheet(self) + stylesheet.set_register(self) self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) @@ -298,7 +298,7 @@ class StatusBar(QWidget): # Turning on is handled in on_current_caret_selection_toggled log.statusbar.debug("Setting caret mode off") self._color_flags.caret = ColorFlags.CaretMode.off - config.set_register_stylesheet(self, update=False) + stylesheet.set_register(self, update=False) def _set_mode_text(self, mode): """Set the mode text.""" @@ -382,7 +382,7 @@ class StatusBar(QWidget): else: self._set_mode_text("caret") self._color_flags.caret = ColorFlags.CaretMode.on - config.set_register_stylesheet(self, update=False) + stylesheet.set_register(self, update=False) def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py index 34c1954e8..389ddf12e 100644 --- a/qutebrowser/mainwindow/statusbar/progress.py +++ b/qutebrowser/mainwindow/statusbar/progress.py @@ -22,7 +22,7 @@ from PyQt5.QtCore import pyqtSlot, QSize from PyQt5.QtWidgets import QProgressBar, QSizePolicy -from qutebrowser.config import config +from qutebrowser.config import stylesheet from qutebrowser.utils import utils, usertypes @@ -45,7 +45,7 @@ class Progress(QProgressBar): def __init__(self, parent=None): super().__init__(parent) - config.set_register_stylesheet(self) + stylesheet.set_register(self) self.enabled = False self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.setTextVisible(False) diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index d6aacb184..7a84b1e4e 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -24,7 +24,7 @@ import enum from PyQt5.QtCore import pyqtSlot, pyqtProperty, QUrl from qutebrowser.mainwindow.statusbar import textbase -from qutebrowser.config import config +from qutebrowser.config import stylesheet from qutebrowser.utils import usertypes, urlutils @@ -76,7 +76,7 @@ class UrlText(textbase.TextBase): super().__init__(parent) self._urltype = None self.setObjectName(self.__class__.__name__) - config.set_register_stylesheet(self) + stylesheet.set_register(self) self._hover_url = None self._normal_url = None self._normal_url_type = UrlType.normal diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index f125a976d..50c9a1d5d 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -553,8 +553,7 @@ class TabbedBrowser(QWidget): self, url: QUrl = None, background: bool = None, related: bool = True, - idx: int = None, *, - ignore_tabs_are_windows: bool = False + idx: int = None, ) -> browsertab.AbstractTab: """Open a new tab with a given URL. @@ -573,8 +572,6 @@ class TabbedBrowser(QWidget): - Explicitly opened tabs are at the very right (related=False) idx: The index where the new tab should be opened. - ignore_tabs_are_windows: If given, never open a new window, even - with tabs.tabs_are_windows set. Return: The opened WebView instance. @@ -587,8 +584,7 @@ class TabbedBrowser(QWidget): prev_focus = QApplication.focusWidget() - if (config.val.tabs.tabs_are_windows and self.widget.count() > 0 and - not ignore_tabs_are_windows): + if config.val.tabs.tabs_are_windows and self.widget.count() > 0: window = mainwindow.MainWindow(private=self.is_private) window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index ba6ffb79f..25ecc69b8 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -32,7 +32,7 @@ from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, from PyQt5.QtGui import QIcon, QPalette, QColor from qutebrowser.utils import qtutils, objreg, utils, usertypes, log -from qutebrowser.config import config +from qutebrowser.config import config, stylesheet from qutebrowser.misc import objects, debugcachestats from qutebrowser.browser import browsertab @@ -116,7 +116,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) + tabbar = self.tabBar() + + if config.cache['tabs.tooltips']: + # always show only plain title in tooltips + tabbar.setTabToolTip(idx, title) + + tabbar.set_tab_data(idx, 'page-title', title) self.update_tab_title(idx) def page_title(self, idx): @@ -153,10 +159,6 @@ class TabWidget(QTabWidget): if tabbar.tabText(idx) != title: tabbar.setTabText(idx, title) - if config.cache['tabs.tooltips']: - # always show only plain title in tooltips - tabbar.setTabToolTip(idx, fields['current_title']) - def get_tab_fields(self, idx): """Get the tab field data.""" tab = self.widget(idx) @@ -400,7 +402,7 @@ class TabBar(QTabBar): self._on_show_switching_delay_changed() self.setAutoFillBackground(True) self.drag_in_progress = False - config.set_register_stylesheet(self) + stylesheet.set_register(self) QTimer.singleShot(0, self.maybe_hide) def __repr__(self): diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 9cc00c6a8..68f9685d3 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -31,7 +31,7 @@ import re from PyQt5.QtWidgets import QLabel, QSizePolicy from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt -from qutebrowser.config import config +from qutebrowser.config import config, stylesheet from qutebrowser.utils import utils, usertypes from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils @@ -72,7 +72,7 @@ class KeyHintView(QLabel): self._show_timer = usertypes.Timer(self, 'keyhint_show') self._show_timer.timeout.connect(self.show) self._show_timer.setSingleShot(True) - config.set_register_stylesheet(self) + stylesheet.set_register(self) def __repr__(self): return utils.get_repr(self, win_id=self._win_id) diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py index f456d93c9..2c02975db 100644 --- a/qutebrowser/utils/javascript.py +++ b/qutebrowser/utils/javascript.py @@ -21,8 +21,6 @@ import typing -from qutebrowser.utils import jinja - _InnerJsArgType = typing.Union[None, str, bool, int, float] _JsArgType = typing.Union[_InnerJsArgType, typing.Sequence[_InnerJsArgType]] @@ -83,5 +81,6 @@ def assemble(module: str, function: str, *args: _JsArgType) -> str: def wrap_global(name: str, *sources: str) -> str: """Wrap a script using window._qutebrowser.""" + from qutebrowser.utils import jinja # circular import template = jinja.js_environment.get_template('global_wrapper.js') return template.render(code='\n'.join(sources), name=name) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 51f0fd6cc..7e7d42e90 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -138,18 +138,15 @@ def _is_url_naive(urlstr: str) -> bool: """ url = qurl_from_user_input(urlstr) assert url.isValid() + host = url.host() - if not utils.raises(ValueError, ipaddress.ip_address, urlstr): - # Valid IPv4/IPv6 address + # Valid IPv4/IPv6 address. Qt converts things like "23.42" or "1337" or + # "0xDEAD" to IP addresses, which we don't like, so we check if the host + # from Qt is part of the input. + if (not utils.raises(ValueError, ipaddress.ip_address, host) and + host in urlstr): return True - # Qt treats things like "23.42" or "1337" or "0xDEAD" as valid URLs - # which we don't want to. Note we already filtered *real* valid IPs - # above. - if not QHostAddress(urlstr).isNull(): - return False - - host = url.host() tld = r'\.([^.0-9_-]+|xn--[a-z0-9-]+)$' forbidden = r'[\u0000-\u002c\u002f\u003a-\u0060\u007b-\u00b6]' return bool(re.search(tld, host) and not re.search(forbidden, host)) @@ -617,12 +614,3 @@ def proxy_from_url(url: QUrl) -> QNetworkProxy: if url.password(): proxy.setPassword(url.password()) return proxy - - -def widened_hostnames(hostname: str) -> typing.Iterable[str]: - """A generator for widening string hostnames. - - Ex: a.c.foo -> [a.c.foo, c.foo, foo]""" - while hostname: - yield hostname - hostname = hostname.partition(".")[-1] diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 5214459bb..65ab2d491 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -25,7 +25,7 @@ import typing import attr from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer -from PyQt5.QtCore import QUrl # pylint: disable=unused-import +from PyQt5.QtCore import QUrl from qutebrowser.utils import log, qtutils, utils @@ -33,14 +33,17 @@ from qutebrowser.utils import log, qtutils, utils _T = typing.TypeVar('_T') -class UnsetObject: +class Unset: """Class for an unset object.""" __slots__ = () + def __repr__(self) -> str: + return '<UNSET>' + -UNSET = UnsetObject() +UNSET = Unset() class NeighborList(typing.Sequence[_T]): @@ -60,7 +63,7 @@ class NeighborList(typing.Sequence[_T]): Modes = enum.Enum('Modes', ['edge', 'exception']) def __init__(self, items: typing.Sequence[_T] = None, - default: typing.Union[_T, UnsetObject] = UNSET, + default: typing.Union[_T, Unset] = UNSET, mode: Modes = Modes.exception) -> None: """Constructor. @@ -79,7 +82,7 @@ class NeighborList(typing.Sequence[_T]): self._items = list(items) self._default = default - if not isinstance(default, UnsetObject): + if not isinstance(default, Unset): idx = self._items.index(default) self._idx = idx # type: typing.Optional[int] else: diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index f3588a5d5..50a00c20b 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -45,11 +45,6 @@ try: except ImportError: # pragma: no cover qWebKitVersion = None # type: ignore # noqa: N816 -try: - from PyQt5.QtWebEngineWidgets import QWebEngineProfile -except ImportError: # pragma: no cover - QWebEngineProfile = None # type: ignore - import qutebrowser from qutebrowser.utils import log, utils, standarddir, usertypes, message from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin @@ -369,18 +364,14 @@ def _chromium_version() -> str: Also see https://www.chromium.org/developers/calendar and https://chromereleases.googleblog.com/ """ - if webenginesettings is None or QWebEngineProfile is None: # type: ignore - # This should never happen + if webenginesettings is None: return 'unavailable' # type: ignore - ua = webenginesettings.default_user_agent - if ua is None: - profile = QWebEngineProfile.defaultProfile() - ua = profile.httpUserAgent() - match = re.search(r' Chrome/([^ ]*) ', ua) - if not match: - log.misc.error("Could not get Chromium version from: {}".format(ua)) - return 'unknown' - return match.group(1) + + if webenginesettings.parsed_user_agent is None: + webenginesettings.init_user_agent() + assert webenginesettings.parsed_user_agent is not None + + return webenginesettings.parsed_user_agent.upstream_browser_version def _backend() -> str: @@ -419,13 +410,13 @@ def version() -> str: if gitver is not None: lines.append("Git commit: {}".format(gitver)) - lines.append("Backend: {}".format(_backend())) + lines.append('Backend: {}'.format(_backend())) + lines.append('Qt: {}'.format(earlyinit.qt_version())) lines += [ '', '{}: {}'.format(platform.python_implementation(), platform.python_version()), - 'Qt: {}'.format(earlyinit.qt_version()), 'PyQt: {}'.format(PYQT_VERSION_STR), '', ] diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index fbf6cb954..7798415a5 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -147,6 +147,8 @@ PERFECT_FILES = [ ('tests/unit/config/test_config.py', 'config/config.py'), + ('tests/unit/config/test_stylesheet.py', + 'config/stylesheet.py'), ('tests/unit/config/test_configdata.py', 'config/configdata.py'), ('tests/unit/config/test_configexc.py', diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index e6b7f9fa4..7a26c9dda 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -362,13 +362,6 @@ Feature: Various utility commands. Then the header User-Agent should be set to toaster And the javascript message "toaster" should be logged - Scenario: Setting the default user-agent header - When I set content.headers.user_agent to <empty> - And I open headers - And I run :jseval console.log(window.navigator.userAgent) - Then the header User-Agent should be set to Mozilla/5.0 * - And the javascript message "Mozilla/5.0 *" should be logged - ## https://github.com/qutebrowser/qutebrowser/issues/1523 Scenario: Completing a single option argument diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index c76d4f061..045be5e94 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -43,7 +43,7 @@ from PyQt5.QtNetwork import QNetworkCookieJar import helpers.stubs as stubsmod from qutebrowser.config import (config, configdata, configtypes, configexc, - configfiles, configcache) + configfiles, configcache, stylesheet) from qutebrowser.api import config as configapi from qutebrowser.utils import objreg, standarddir, utils, usertypes from qutebrowser.browser import greasemonkey, history, qutescheme @@ -204,12 +204,13 @@ def web_tab_setup(qtbot, tab_registry, session_manager_stub, @pytest.fixture def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager, - widget_container): + widget_container, webpage): webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab') tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager, private=False) widget_container.set_widget(tab) + return tab @@ -318,6 +319,9 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub): pass conf.val = container # For easier use in tests + + stylesheet.init() + return conf @@ -416,6 +420,7 @@ def webengineview(qtbot, monkeypatch, web_tab_setup): def webpage(qnam): """Get a new QWebPage object.""" QtWebKitWidgets = pytest.importorskip('PyQt5.QtWebKitWidgets') + class WebPageStub(QtWebKitWidgets.QWebPage): """QWebPage with default error pages disabled.""" @@ -425,8 +430,13 @@ def webpage(qnam): return False page = WebPageStub() + page.networkAccessManager().deleteLater() page.setNetworkAccessManager(qnam) + + from qutebrowser.browser.webkit import webkitsettings + webkitsettings._init_user_agent() + return page diff --git a/tests/unit/browser/webengine/test_webengineinterceptor.py b/tests/unit/browser/webengine/test_webengineinterceptor.py index 9ea8ddcf5..e4df5f460 100644 --- a/tests/unit/browser/webengine/test_webengineinterceptor.py +++ b/tests/unit/browser/webengine/test_webengineinterceptor.py @@ -27,13 +27,23 @@ pytest.importorskip('PyQt5.QtWebEngineWidgets') from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInfo from qutebrowser.browser.webengine import interceptor - - -class TestWebengineInterceptor: - - def test_requestinfo_dict_valid(self): - """Test that the RESOURCE_TYPES dict is not missing any values.""" - qb_keys = interceptor.RequestInterceptor.RESOURCE_TYPES.keys() - qt_keys = {i for i in vars(QWebEngineUrlRequestInfo).values() - if isinstance(i, QWebEngineUrlRequestInfo.ResourceType)} - assert qt_keys == qb_keys +from qutebrowser.extensions import interceptors +from qutebrowser.utils import qtutils + + +def test_no_missing_resource_types(): + request_interceptor = interceptor.RequestInterceptor() + qb_keys = request_interceptor._resource_types.keys() + qt_keys = {i for i in vars(QWebEngineUrlRequestInfo).values() + if isinstance(i, QWebEngineUrlRequestInfo.ResourceType)} + assert qt_keys == qb_keys + + +def test_resource_type_values(): + request_interceptor = interceptor.RequestInterceptor() + for qt_value, qb_item in request_interceptor._resource_types.items(): + if (qtutils.version_check('5.7.1', exact=True, compiled=False) and + qb_item == interceptors.ResourceType.unknown): + # Qt 5.7 has ResourceTypeUnknown = 18 instead of 255 + continue + assert qt_value == qb_item.value diff --git a/tests/unit/browser/webengine/test_webenginesettings.py b/tests/unit/browser/webengine/test_webenginesettings.py index 0e369d655..d2a6b96ba 100644 --- a/tests/unit/browser/webengine/test_webenginesettings.py +++ b/tests/unit/browser/webengine/test_webenginesettings.py @@ -33,6 +33,7 @@ from qutebrowser.misc import objects def init(qapp, config_stub, cache_tmpdir, data_tmpdir, monkeypatch): monkeypatch.setattr(webenginesettings.webenginequtescheme, 'init', lambda: None) + monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) init_args = types.SimpleNamespace(enable_webengine_inspector=False) webenginesettings.init(init_args) config_stub.changed.disconnect(webenginesettings._update_settings) @@ -49,7 +50,6 @@ def test_big_cache_size(config_stub): @pytest.mark.skipif( not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer") def test_non_existing_dict(config_stub, monkeypatch, message_mock, caplog): - monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) monkeypatch.setattr(webenginesettings.spell, 'local_filename', lambda _code: None) config_stub.val.spellcheck.languages = ['af-ZA'] @@ -66,7 +66,6 @@ def test_non_existing_dict(config_stub, monkeypatch, message_mock, caplog): @pytest.mark.skipif( not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer") def test_existing_dict(config_stub, monkeypatch): - monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) monkeypatch.setattr(webenginesettings.spell, 'local_filename', lambda _code: 'en-US-8-0') config_stub.val.spellcheck.languages = ['en-US'] @@ -80,7 +79,6 @@ def test_existing_dict(config_stub, monkeypatch): @pytest.mark.skipif( not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer") def test_spell_check_disabled(config_stub, monkeypatch): - monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) config_stub.val.spellcheck.languages = [] webenginesettings._update_settings('spellcheck.languages') for profile in [webenginesettings.default_profile, @@ -89,4 +87,11 @@ def test_spell_check_disabled(config_stub, monkeypatch): def test_default_user_agent_saved(): - assert webenginesettings.default_user_agent is not None + assert webenginesettings.parsed_user_agent is not None + + +def test_parsed_user_agent(qapp): + webenginesettings.init_user_agent() + parsed = webenginesettings.parsed_user_agent + assert parsed.upstream_browser_key == 'Chrome' + assert parsed.qt_key == 'QtWebEngine' diff --git a/tests/unit/browser/webkit/test_webkitsettings.py b/tests/unit/browser/webkit/test_webkitsettings.py new file mode 100644 index 000000000..bb7fbecb8 --- /dev/null +++ b/tests/unit/browser/webkit/test_webkitsettings.py @@ -0,0 +1,31 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2019 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +import pytest +pytest.importorskip('PyQt5.QtWebKitWidgets') + +from qutebrowser.browser.webkit import webkitsettings + + +def test_parsed_user_agent(qapp): + webkitsettings._init_user_agent() + + parsed = webkitsettings.parsed_user_agent + assert parsed.upstream_browser_key == 'Version' + assert parsed.qt_key == 'Qt' diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 9eefedf15..d81cb91dd 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -23,10 +23,10 @@ import unittest.mock import functools import pytest -from PyQt5.QtCore import QObject, QUrl +from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor -from qutebrowser.config import config, configdata, configexc, configutils +from qutebrowser.config import config, configdata, configexc from qutebrowser.utils import usertypes, urlmatch from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils @@ -410,7 +410,7 @@ class TestConfig: assert conf.get(name) == 'always' if save_yaml: - assert yaml_value(name) is configutils.UNSET + assert yaml_value(name) is usertypes.UNSET else: assert yaml_value(name) == 'never' @@ -439,8 +439,8 @@ class TestConfig: assert options == {name1, name2} if save_yaml: - assert yaml_value(name1) is configutils.UNSET - assert yaml_value(name2) is configutils.UNSET + assert yaml_value(name1) is usertypes.UNSET + assert yaml_value(name2) is usertypes.UNSET else: assert yaml_value(name1) == 'never' assert yaml_value(name2) is True @@ -482,7 +482,7 @@ class TestConfig: @pytest.mark.parametrize('fallback, expected', [ (True, True), - (False, configutils.UNSET) + (False, usertypes.UNSET) ]) def test_get_for_url_fallback(self, conf, fallback, expected): """Test conf.get() with a URL and fallback.""" @@ -617,7 +617,7 @@ class TestConfig: pattern = urlmatch.UrlPattern('*://example.com') name = 'content.javascript.enabled' value = conf.get_obj_for_pattern(name, pattern=pattern) - assert value is configutils.UNSET + assert value is usertypes.UNSET def test_get_str(self, conf): assert conf.get_str('content.plugins') == 'false' @@ -637,7 +637,7 @@ class TestConfig: if save_yaml: assert yaml_value(option) is True else: - assert yaml_value(option) is configutils.UNSET + assert yaml_value(option) is usertypes.UNSET @pytest.mark.parametrize('method', ['set_obj', 'set_str']) def test_set_invalid(self, conf, qtbot, method): @@ -669,7 +669,7 @@ class TestConfig: meth(option, value, save_yaml=True) assert not conf._values[option] - assert yaml_value(option) is configutils.UNSET + assert yaml_value(option) is usertypes.UNSET @pytest.mark.parametrize('method, value', [ ('set_obj', {}), @@ -766,57 +766,3 @@ class TestContainer: with pytest.raises(TypeError, match="Can't use pattern without configapi!"): config.ConfigContainer(config_stub, pattern=pattern) - - -class StyleObj(QObject): - - def __init__(self, stylesheet=None, parent=None): - super().__init__(parent) - if stylesheet is not None: - self.STYLESHEET = stylesheet # noqa: N801,N806 pylint: disable=invalid-name - self.rendered_stylesheet = None - - def setStyleSheet(self, stylesheet): - self.rendered_stylesheet = stylesheet - - -def test_get_stylesheet(config_stub): - config_stub.val.colors.hints.fg = 'magenta' - observer = config.StyleSheetObserver( - StyleObj(), stylesheet="{{ conf.colors.hints.fg }}", update=False) - assert observer._get_stylesheet() == 'magenta' - - -@pytest.mark.parametrize('delete', [True, False]) -@pytest.mark.parametrize('stylesheet_param', [True, False]) -@pytest.mark.parametrize('update', [True, False]) -def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot, - config_stub, caplog): - config_stub.val.colors.hints.fg = 'magenta' - stylesheet = "{{ conf.colors.hints.fg }}" - - with caplog.at_level(9): # VDEBUG - if stylesheet_param: - obj = StyleObj() - config.set_register_stylesheet(obj, stylesheet=stylesheet, - update=update) - else: - obj = StyleObj(stylesheet) - config.set_register_stylesheet(obj, update=update) - - assert caplog.messages[-1] == 'stylesheet for StyleObj: magenta' - - assert obj.rendered_stylesheet == 'magenta' - - if delete: - with qtbot.waitSignal(obj.destroyed): - obj.deleteLater() - - config_stub.val.colors.hints.fg = 'yellow' - - if delete or not update: - expected = 'magenta' - else: - expected = 'yellow' - - assert obj.rendered_stylesheet == expected diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 382d41ad8..97dcd7c42 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -25,7 +25,7 @@ import unittest.mock import pytest from PyQt5.QtCore import QUrl -from qutebrowser.config import configcommands, configutils +from qutebrowser.config import configcommands from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes, urlmatch from qutebrowser.keyinput import keyutils @@ -92,7 +92,7 @@ class TestSet: commands.set(0, option, inp, temp=temp) assert config_stub.get(option) == new_value - assert yaml_value(option) == (configutils.UNSET if temp else new_value) + assert yaml_value(option) == (usertypes.UNSET if temp else new_value) def test_set_with_pattern(self, monkeypatch, commands, config_stub): monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit) @@ -295,7 +295,7 @@ class TestAdd: assert str(config_stub.get(name)[-1]) == value if temp: - assert yaml_value(name) == configutils.UNSET + assert yaml_value(name) == usertypes.UNSET else: assert yaml_value(name)[-1] == value @@ -328,7 +328,7 @@ class TestAdd: assert str(config_stub.get(name)[key]) == value if temp: - assert yaml_value(name) == configutils.UNSET + assert yaml_value(name) == usertypes.UNSET else: assert yaml_value(name)[key] == value @@ -379,7 +379,7 @@ class TestRemove: assert value not in config_stub.get(name) if temp: - assert yaml_value(name) == configutils.UNSET + assert yaml_value(name) == usertypes.UNSET else: assert value not in yaml_value(name) @@ -410,7 +410,7 @@ class TestRemove: assert key not in config_stub.get(name) if temp: - assert yaml_value(name) == configutils.UNSET + assert yaml_value(name) == usertypes.UNSET else: assert key not in yaml_value(name) @@ -446,7 +446,7 @@ class TestUnsetAndClear: commands.config_unset(name, temp=temp) assert config_stub.get(name) == 'always' - assert yaml_value(name) == ('never' if temp else configutils.UNSET) + assert yaml_value(name) == ('never' if temp else usertypes.UNSET) def test_unset_unknown_option(self, commands): with pytest.raises(cmdutils.CommandError, match="No option 'tabs'"): @@ -460,7 +460,7 @@ class TestUnsetAndClear: commands.config_clear(save=save) assert config_stub.get(name) == 'always' - assert yaml_value(name) == (configutils.UNSET if save else 'never') + assert yaml_value(name) == (usertypes.UNSET if save else 'never') class TestSource: diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 833e1e4fd..36bf88868 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -354,6 +354,17 @@ class TestYaml: def test_title_format_migrations(self, migration_test, setting, old, new): migration_test(setting, old, new) + @pytest.mark.parametrize('old, new', [ + (None, ('Mozilla/5.0 ({os_info}) ' + 'AppleWebKit/{webkit_version} (KHTML, like Gecko) ' + '{qt_key}/{qt_version} ' + '{upstream_browser_key}/{upstream_browser_version} ' + 'Safari/{webkit_version}')), + ('toaster', 'toaster'), + ]) + def test_user_agent_migration(self, migration_test, old, new): + migration_test('content.headers.user_agent', old, new) + def test_renamed_key_unknown_target(self, monkeypatch, yaml, autoconfig): """A key marked as renamed with invalid name should raise an error.""" diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 671653b94..9d2843f63 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -308,6 +308,7 @@ class TestEarlyInit: ('qt.force_software_rendering', 'chromium', 'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND', '1'), ('qt.force_platform', 'toaster', 'QT_QPA_PLATFORM', 'toaster'), + ('qt.force_platformtheme', 'lxde', 'QT_QPA_PLATFORMTHEME', 'lxde'), ('window.hide_decoration', True, 'QT_WAYLAND_DISABLE_WINDOWDECORATION', '1') ]) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index e7b483dd8..e766fca8a 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -34,8 +34,8 @@ from PyQt5.QtGui import QColor, QFont from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.misc import objects -from qutebrowser.config import configtypes, configexc, configutils -from qutebrowser.utils import debug, utils, qtutils, urlmatch +from qutebrowser.config import configtypes, configexc +from qutebrowser.utils import debug, utils, qtutils, urlmatch, usertypes from qutebrowser.browser.network import pac from qutebrowser.keyinput import keyutils from helpers import utils as testutils @@ -277,7 +277,7 @@ class TestAll: @pytest.mark.parametrize('none_ok', [True, False]) def test_unset(self, klass, none_ok): typ = klass(none_ok=none_ok) - assert typ.to_py(configutils.UNSET) is configutils.UNSET + assert typ.to_py(usertypes.UNSET) is usertypes.UNSET def test_to_str_none(self, klass): assert klass().to_str(None) == '' @@ -1838,8 +1838,12 @@ class TestDirectory: class TestFormatString: @pytest.fixture - def typ(self): - return configtypes.FormatString(fields=('foo', 'bar')) + def klass(self): + return configtypes.FormatString + + @pytest.fixture + def typ(self, klass): + return klass(fields=('foo', 'bar')) @pytest.mark.parametrize('val', [ 'foo bar baz', @@ -1857,6 +1861,14 @@ class TestFormatString: with pytest.raises(configexc.ValidationError): typ.to_py(val) + @pytest.mark.parametrize('value', [ + None, + ['one', 'two'], + [('1', 'one'), ('2', 'two')], + ]) + def test_complete(self, klass, value): + assert klass(fields=('foo'), completions=value).complete() == value + class TestShellCommand: diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py index e87e4ab93..2c99ec34b 100644 --- a/tests/unit/config/test_configutils.py +++ b/tests/unit/config/test_configutils.py @@ -23,19 +23,10 @@ import pytest from PyQt5.QtCore import QUrl from qutebrowser.config import configutils, configdata, configtypes -from qutebrowser.utils import urlmatch +from qutebrowser.utils import urlmatch, usertypes from tests.helpers import utils -def test_unset_object_identity(): - assert configutils.Unset() is not configutils.Unset() - assert configutils.UNSET is configutils.UNSET - - -def test_unset_object_repr(): - assert repr(configutils.UNSET) == '<UNSET>' - - @pytest.fixture def opt(): return configdata.Option(name='example.option', typ=configtypes.String(), @@ -62,6 +53,14 @@ def values(opt, pattern): @pytest.fixture +def mixed_values(opt, pattern): + scoped_values = [configutils.ScopedValue('global value', None), + configutils.ScopedValue('example value', pattern, + hide_userconfig=True)] + return configutils.Values(opt, scoped_values) + + +@pytest.fixture def empty_values(opt): return configutils.Values(opt) @@ -78,6 +77,23 @@ def test_str_empty(empty_values): assert str(empty_values) == 'example.option: <unchanged>' +def test_str_mixed(mixed_values): + expected = [ + 'example.option = global value', + '*://www.example.com/: example.option = example value', + ] + assert str(mixed_values) == '\n'.join(expected) + + +@pytest.mark.parametrize('include_hidden, expected', [ + (True, ['example.option = global value', + '*://www.example.com/: example.option = example value']), + (False, ['example.option = global value']), +]) +def test_dump(mixed_values, include_hidden, expected): + assert mixed_values.dump(include_hidden=include_hidden) == expected + + def test_bool(values, empty_values): assert values assert not empty_values @@ -121,7 +137,7 @@ def test_clear(values): assert values values.clear() assert not values - assert values.get_for_url(fallback=False) is configutils.UNSET + assert values.get_for_url(fallback=False) is usertypes.UNSET def test_get_matching(values): @@ -130,12 +146,12 @@ def test_get_matching(values): def test_get_unset(empty_values): - assert empty_values.get_for_url(fallback=False) is configutils.UNSET + assert empty_values.get_for_url(fallback=False) is usertypes.UNSET def test_get_no_global(empty_values, other_pattern, pattern): empty_values.add('example.org value', pattern) - assert empty_values.get_for_url(fallback=False) is configutils.UNSET + assert empty_values.get_for_url(fallback=False) is usertypes.UNSET def test_get_unset_fallback(empty_values): @@ -144,7 +160,7 @@ def test_get_unset_fallback(empty_values): def test_get_non_matching(values): url = QUrl('https://www.example.ch/') - assert values.get_for_url(url, fallback=False) is configutils.UNSET + assert values.get_for_url(url, fallback=False) is usertypes.UNSET def test_get_non_matching_fallback(values): @@ -180,13 +196,13 @@ def test_get_pattern_none(values, pattern): def test_get_unset_pattern(empty_values, pattern): value = empty_values.get_for_pattern(pattern, fallback=False) - assert value is configutils.UNSET + assert value is usertypes.UNSET def test_get_no_global_pattern(empty_values, pattern, other_pattern): empty_values.add('example.org value', other_pattern) value = empty_values.get_for_pattern(pattern, fallback=False) - assert value is configutils.UNSET + assert value is usertypes.UNSET def test_get_unset_fallback_pattern(empty_values, pattern): @@ -195,7 +211,7 @@ def test_get_unset_fallback_pattern(empty_values, pattern): def test_get_non_matching_pattern(values, other_pattern): value = values.get_for_pattern(other_pattern, fallback=False) - assert value is configutils.UNSET + assert value is usertypes.UNSET def test_get_non_matching_fallback_pattern(values, other_pattern): @@ -238,3 +254,26 @@ def test_domain_lookup_sparse_benchmark(url, values, benchmark): values.add(False, urlmatch.UrlPattern(line)) benchmark(lambda: values.get_for_url(url)) + + +class TestWiden: + + @pytest.mark.parametrize('hostname, expected', [ + ('a.b.c', ['a.b.c', 'b.c', 'c']), + ('foobarbaz', ['foobarbaz']), + ('', []), + ('.c', ['.c', 'c']), + ('c.', ['c.']), + ('.c.', ['.c.', 'c.']), + (None, []), + ]) + def test_widen_hostnames(self, hostname, expected): + assert list(configutils._widened_hostnames(hostname)) == expected + + @pytest.mark.parametrize('hostname', [ + 'test.qutebrowser.org', + 'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.z.y.z', + 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq.c', + ]) + def test_bench_widen_hostnames(self, hostname, benchmark): + benchmark(lambda: list(configutils._widened_hostnames(hostname))) diff --git a/tests/unit/config/test_stylesheet.py b/tests/unit/config/test_stylesheet.py new file mode 100644 index 000000000..67bdd04b4 --- /dev/null +++ b/tests/unit/config/test_stylesheet.py @@ -0,0 +1,72 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: +# Copyright 2019 Florian Bruhin (The Compiler) <mail@qutebrowser.org> + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +import pytest + +from PyQt5.QtCore import QObject + +from qutebrowser.config import stylesheet + + +class StyleObj(QObject): + + def __init__(self, stylesheet=None, parent=None): + super().__init__(parent) + if stylesheet is not None: + self.STYLESHEET = stylesheet # noqa: N801,N806 pylint: disable=invalid-name + self.rendered_stylesheet = None + + def setStyleSheet(self, stylesheet): + self.rendered_stylesheet = stylesheet + + +def test_get_stylesheet(config_stub): + config_stub.val.colors.hints.fg = 'magenta' + observer = stylesheet._StyleSheetObserver( + StyleObj(), stylesheet="{{ conf.colors.hints.fg }}", update=False) + assert observer._get_stylesheet() == 'magenta' + + +@pytest.mark.parametrize('delete', [True, False]) +@pytest.mark.parametrize('stylesheet_param', [True, False]) +@pytest.mark.parametrize('update', [True, False]) +def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot, + config_stub, caplog): + config_stub.val.colors.hints.fg = 'magenta' + qss = "{{ conf.colors.hints.fg }}" + + with caplog.at_level(9): # VDEBUG + if stylesheet_param: + obj = StyleObj() + stylesheet.set_register(obj, qss, update=update) + else: + obj = StyleObj(qss) + stylesheet.set_register(obj, update=update) + + assert caplog.messages[-1] == 'stylesheet for StyleObj: magenta' + + assert obj.rendered_stylesheet == 'magenta' + + if delete: + with qtbot.waitSignal(obj.destroyed): + obj.deleteLater() + + config_stub.val.colors.hints.fg = 'yellow' + + expected = 'magenta' if delete or not update else 'yellow' + assert obj.rendered_stylesheet == expected diff --git a/tests/unit/config/test_websettings.py b/tests/unit/config/test_websettings.py new file mode 100644 index 000000000..b00f24af9 --- /dev/null +++ b/tests/unit/config/test_websettings.py @@ -0,0 +1,104 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2019 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +import pytest + +from qutebrowser.config import websettings +from qutebrowser.misc import objects +from qutebrowser.utils import usertypes + + +@pytest.mark.parametrize([ + 'user_agent', 'os_info', 'webkit_version', + 'upstream_browser_key', 'upstream_browser_version', 'qt_key' +], [ + ( + # QtWebEngine, Linux + # (no differences other than Chrome version with older Qt Versions) + ("Mozilla/5.0 (X11; Linux x86_64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "QtWebEngine/5.14.0 Chrome/77.0.3865.98 Safari/537.36"), + "X11; Linux x86_64", + "537.36", + "Chrome", "77.0.3865.98", + "QtWebEngine", + ), ( + # QtWebKit, Linux + ("Mozilla/5.0 (X11; Linux x86_64) " + "AppleWebKit/602.1 (KHTML, like Gecko) " + "qutebrowser/1.8.3 " + "Version/10.0 Safari/602.1"), + "X11; Linux x86_64", + "602.1", + "Version", "10.0", + "Qt", + ), ( + # QtWebEngine, macOS + ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "QtWebEngine/5.13.2 Chrome/73.0.3683.105 Safari/537.36"), + "Macintosh; Intel Mac OS X 10_12_6", + "537.36", + "Chrome", "73.0.3683.105", + "QtWebEngine", + ), ( + # QtWebEngine, Windows + ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "QtWebEngine/5.12.5 Chrome/69.0.3497.128 Safari/537.36"), + "Windows NT 10.0; Win64; x64", + "537.36", + "Chrome", "69.0.3497.128", + "QtWebEngine", + ) +]) +def test_parse_user_agent(user_agent, os_info, webkit_version, + upstream_browser_key, upstream_browser_version, + qt_key): + parsed = websettings.UserAgent.parse(user_agent) + assert parsed.os_info == os_info + assert parsed.webkit_version == webkit_version + assert parsed.upstream_browser_key == upstream_browser_key + assert parsed.upstream_browser_version == upstream_browser_version + assert parsed.qt_key == qt_key + + +def test_user_agent(monkeypatch, config_stub, qapp): + webenginesettings = pytest.importorskip( + "qutebrowser.browser.webengine.webenginesettings") + monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) + webenginesettings.init_user_agent() + + config_stub.val.content.headers.user_agent = 'test {qt_key}' + assert websettings.user_agent() == 'test QtWebEngine' + + config_stub.val.content.headers.user_agent = 'test2 {qt_key}' + assert websettings.user_agent() == 'test2 QtWebEngine' + + +def test_config_init(request, monkeypatch, config_stub): + if request.config.webengine: + from qutebrowser.browser.webengine import webenginesettings + monkeypatch.setattr(webenginesettings, 'init', lambda _args: None) + else: + from qutebrowser.browser.webkit import webkitsettings + monkeypatch.setattr(webkitsettings, 'init', lambda _args: None) + + websettings.init(args=None) + assert config_stub.dump_userconfig() == '<Default configuration>' diff --git a/tests/unit/javascript/stylesheet/test_stylesheet.py b/tests/unit/javascript/stylesheet/test_stylesheet_js.py index 768ffaeb9..768ffaeb9 100644 --- a/tests/unit/javascript/stylesheet/test_stylesheet.py +++ b/tests/unit/javascript/stylesheet/test_stylesheet_js.py diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index bf1ce47c8..49cee6382 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -346,7 +346,9 @@ def test_get_search_url_invalid(url): (True, True, False, '127.0.0.1'), (True, True, False, '::1'), (True, True, True, '2001:41d0:2:6c11::1'), + (True, True, True, '[2001:41d0:2:6c11::1]:8000'), (True, True, True, '94.23.233.17'), + (True, True, True, '94.23.233.17:8000'), # Special URLs (True, True, False, 'file:///tmp/foo'), (True, True, False, 'about:blank'), @@ -706,26 +708,3 @@ class TestProxyFromUrl: def test_invalid(self, url, exception): with pytest.raises(exception): urlutils.proxy_from_url(QUrl(url)) - - -class TestWiden: - - @pytest.mark.parametrize('hostname, expected', [ - ('a.b.c', ['a.b.c', 'b.c', 'c']), - ('foobarbaz', ['foobarbaz']), - ('', []), - ('.c', ['.c', 'c']), - ('c.', ['c.']), - ('.c.', ['.c.', 'c.']), - (None, []), - ]) - def test_widen_hostnames(self, hostname, expected): - assert list(urlutils.widened_hostnames(hostname)) == expected - - @pytest.mark.parametrize('hostname', [ - 'test.qutebrowser.org', - 'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.z.y.z', - 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq.c', - ]) - def test_bench_widen_hostnames(self, hostname, benchmark): - benchmark(lambda: list(urlutils.widened_hostnames(hostname))) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index b630a6159..d1f692540 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -851,36 +851,36 @@ class FakeQSslSocket: return self._version -@pytest.mark.parametrize('ua, expected', [ - (None, 'unavailable'), # No QWebEngineProfile - ('Mozilla/5.0', 'unknown'), - ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' - 'QtWebEngine/5.8.0 Chrome/53.0.2785.148 Safari/537.36', '53.0.2785.148'), -]) -def test_chromium_version(monkeypatch, caplog, ua, expected): +_QTWE_USER_AGENT = ("Mozilla/5.0 (X11; Linux x86_64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "QtWebEngine/5.14.0 Chrome/{} Safari/537.36") + + +def test_chromium_version(monkeypatch, caplog): pytest.importorskip('PyQt5.QtWebEngineWidgets') - if ua is None: - monkeypatch.setattr(version, 'webenginesettings', None) - else: - monkeypatch.setattr(version.webenginesettings, - 'default_user_agent', ua) - with caplog.at_level(logging.ERROR): - assert version._chromium_version() == expected + ver = '77.0.3865.98' + version.webenginesettings._init_user_agent_str( + _QTWE_USER_AGENT.format(ver)) + + assert version._chromium_version() == ver + + +def test_chromium_version_no_webengine(monkeypatch): + monkeypatch.setattr(version, 'webenginesettings', None) + assert version._chromium_version() == 'unavailable' def test_chromium_version_prefers_saved_user_agent(monkeypatch): pytest.importorskip('PyQt5.QtWebEngineWidgets') - monkeypatch.setattr( - version.webenginesettings, 'default_user_agent', - 'QtWebEngine/5.8.0 Chrome/53.0.2785.148 Safari/537.36' - ) + version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT) class FakeProfile: def defaultProfile(self): raise AssertionError("Should not be called") - monkeypatch.setattr(version, 'QWebEngineProfile', FakeProfile()) + monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile', + FakeProfile()) version._chromium_version() @@ -918,12 +918,9 @@ class VersionParams: ], ids=lambda param: param.name) def test_version_output(params, stubs, monkeypatch, config_stub): """Test version.version().""" - class FakeWebEngineSettings: - default_user_agent = ('Toaster/4.0.4 Chrome/CHROMIUMVERSION ' - 'Teapot/4.1.8') - config.instance.config_py_loaded = params.config_py_loaded import_path = os.path.abspath('/IMPORTPATH') + patches = { 'qutebrowser.__file__': os.path.join(import_path, '__init__.py'), 'qutebrowser.__version__': 'VERSION', @@ -960,6 +957,12 @@ def test_version_output(params, stubs, monkeypatch, config_stub): 'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no", } + ua = _QTWE_USER_AGENT.format('CHROMIUMVERSION') + if version.webenginesettings is None: + patches['_chromium_version'] = lambda: 'CHROMIUMVERSION' + else: + version.webenginesettings._init_user_agent_str(ua) + if params.config_py_loaded: substitutions["config_py_loaded"] = "{} has been loaded".format( standarddir.config_py()) @@ -975,8 +978,6 @@ def test_version_output(params, stubs, monkeypatch, config_stub): monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False) patches['objects.backend'] = usertypes.Backend.QtWebEngine substitutions['backend'] = 'QtWebEngine (Chromium CHROMIUMVERSION)' - patches['webenginesettings'] = FakeWebEngineSettings - patches['QWebEngineProfile'] = True if params.known_distribution: patches['distribution'] = lambda: version.DistributionInfo( @@ -1003,9 +1004,9 @@ def test_version_output(params, stubs, monkeypatch, config_stub): template = textwrap.dedent(""" qutebrowser vVERSION{git_commit} Backend: {backend} + Qt: {qt} PYTHON IMPLEMENTATION: PYTHON VERSION - Qt: {qt} PyQt: PYQT VERSION MODULE VERSION 1 diff --git a/tests/unit/utils/usertypes/test_misc.py b/tests/unit/utils/usertypes/test_misc.py index 1700b7f51..68eabc213 100644 --- a/tests/unit/utils/usertypes/test_misc.py +++ b/tests/unit/utils/usertypes/test_misc.py @@ -25,3 +25,12 @@ def test_abstract_certificate_error_wrapper(): err = object() wrapper = usertypes.AbstractCertificateErrorWrapper(err) assert wrapper._error is err + + +def test_unset_object_identity(): + assert usertypes.Unset() is not usertypes.Unset() + assert usertypes.UNSET is usertypes.UNSET + + +def test_unset_object_repr(): + assert repr(usertypes.UNSET) == '<UNSET>' @@ -13,8 +13,8 @@ skipsdist = true setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms PYTEST_QT_API=pyqt5 - pyqt{,57,59,510,511,512,513}: LINK_PYQT_SKIP=true - pyqt{,57,59,510,511,512,513}: QUTE_BDD_WEBENGINE=true + pyqt{,57,59,510,511,512,513,514}: LINK_PYQT_SKIP=true + pyqt{,57,59,510,511,512,513,514}: QUTE_BDD_WEBENGINE=true cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* DOCKER QT_QUICK_BACKEND basepython = @@ -32,6 +32,7 @@ deps = pyqt511: -r{toxinidir}/misc/requirements/requirements-pyqt-5.11.txt pyqt512: -r{toxinidir}/misc/requirements/requirements-pyqt-5.12.txt pyqt513: -r{toxinidir}/misc/requirements/requirements-pyqt-5.13.txt + pyqt514: -r{toxinidir}/misc/requirements/requirements-pyqt-5.14.txt commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -bb -m pytest {posargs:tests} |