diff --git a/.travis.yml b/.travis.yml index fd1705e40..18d1dd416 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,18 +6,12 @@ python: 3.6 matrix: include: - - os: linux - env: DOCKER=debian-jessie - services: docker - os: linux env: DOCKER=archlinux services: docker - os: linux env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true services: docker - - os: linux - env: DOCKER=ubuntu-xenial - services: docker - os: linux env: TESTENV=py36-pyqt571 - os: linux diff --git a/README.asciidoc b/README.asciidoc index 7a1af16e9..652ea71b3 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -99,11 +99,8 @@ Requirements The following software and libraries are required to run qutebrowser: -* http://www.python.org/[Python] 3.4 or newer (3.6 recommended) - note that - support for Python 3.4 - https://github.com/qutebrowser/qutebrowser/issues/2742[will be dropped soon]. -* http://qt.io/[Qt] 5.2.0 or newer (5.9 recommended - note that support for Qt - < 5.7.1 will be dropped soon) with the following modules: +* http://www.python.org/[Python] 3.5 or newer (3.6 recommended) +* http://qt.io/[Qt] 5.7.1 or newer with the following modules: - QtCore / qtbase - QtQuick (part of qtbase in some distributions) - QtSQL (part of qtbase in some distributions) @@ -111,9 +108,8 @@ The following software and libraries are required to run qutebrowser: - QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG). Note that support for legacy QtWebKit (before 5.212) will be dropped soon. -* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer - (5.9 recommended) for Python 3. Note that support for PyQt < 5.7 will be - dropped soon. +* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer + (5.9 recommended) for Python 3. * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * http://fdik.org/pyPEG/[pyPEG2] * http://jinja.pocoo.org/[jinja2] diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index a1bc8caa3..9cd0c4792 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -22,8 +22,8 @@ Breaking changes - (TODO) Support for legacy QtWebKit (before 5.212 which is distributed independently from Qt) is dropped. -- (TODO) Support for Python 3.4 is dropped. -- (TODO) Support for Qt before 5.7 is dropped. +- Support for Python 3.4 is dropped. +- Support for Qt before 5.7.1 and PyQt before 5.7 is dropped. - (TODO) New dependency on ruamel.yaml; dropped PyYAML dependency. - (TODO) The QtWebEngine backend is now used by default if available. - New dependency on the QtSql module and Qt sqlite support. @@ -36,6 +36,7 @@ Breaking changes work properly anymore. - Various documentation files got moved to the doc/ subfolder, `qutebrowser.desktop` got moved to misc/. +- The `--harfbuzz` commandline argument got dropped Major changes ~~~~~~~~~~~~~ @@ -50,6 +51,9 @@ Added - New back/forward indicator in the statusbar - New `bindings.key_mappings` setting to map keys to other keys - New `qt_args` setting to pass additional arguments to Qt/Chromium +- New `backend` setting to select the backend to use (auto/webengine/webkit). + Together with the previous setting, this should make wrapper scripts + unnecessary. Changed ~~~~~~~ diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index 4b02c4c12..1b2f4be4e 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -180,7 +180,7 @@ In the _scripts/_ subfolder there's a `run_profile.py` which profiles the code and shows a graphical representation of what takes how much time. It uses the built-in Python -https://docs.python.org/3.4/library/profile.html[cProfile] module and can show +https://docs.python.org/3.6/library/profile.html[cProfile] module and can show the output in four different ways: * Raw profile file (`--profile-tool=none`) @@ -535,11 +535,11 @@ ____ Setting up a Windows Development Environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* Install https://www.python.org/downloads/release/python-344/[Python 3.4] -* Install https://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-5.5.1/[PyQt 5.5] -* Create a file at `C:\Windows\system32\python3.bat` with the following content: - `@C:\Python34\python %*` - This will make the Python 3.4 interpreter available as `python3`, which is used by various development scripts. +* Install https://www.python.org/downloads/release/python-362/[Python 3.6]. +* Install PyQt via `pip install PyQt5` +* Create a file at `C:\Windows\system32\python3.bat` with the following content (adjust the path as necessary): + `@C:\Python36\python %*` + This will make the Python 3.6 interpreter available as `python3`, which is used by various development scripts. * Install git from the https://git-scm.com/download/win[git-scm downloads page] Try not to enable `core.autocrlf`, since that will cause `flake8` to complain a lot. Use an editor that can deal with plain line feeds instead. * Clone your favourite qutebrowser repository. diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index cfc16ec12..c46286950 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -205,26 +205,6 @@ Experiencing freezing on sites like duckduckgo and youtube.:: See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357] for more details. -Experiencing segfaults (crashes) on Debian systems.:: - For Debian it's highly recommended to install the `gstreamer0.10-plugins-base` package. - This is a workaround for a bug in Qt, it has been fixed upstream in Qt 5.4 - More details can be found - https://bugs.webkit.org/show_bug.cgi?id=119951[here]. - -Segfaults on Facebook, Medium, Amazon, ...:: - If you are on a Debian or Ubuntu based system, you might experience some crashes - visiting these sites. This is caused by various bugs in Qt which have been - fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade - some packages. On Debian Jessie, it's recommended to use the experimental - repos as described in https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc#on-debian--ubuntu[the documentation]. -+ -Since Ubuntu Trusty (using Qt 5.2.1), -https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over -70 important bugs] have been fixed in QtWebKit. For Debian Jessie (using Qt 5.3.2) -it's still -https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[nearly -20 important bugs]. - When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro:: As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. + As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. + diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index aff506d7e..907e5d7c1 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -12,6 +12,7 @@ |<>|Aliases for commands. |<>|How often (in milliseconds) to auto-save config/cookies/etc. |<>|Always restore open sites when qutebrowser is reopened. +|<>|The backend to use to display websites. |<>|Keybindings mapping keys to commands in different modes. |<>|Default keybindings. If you want to add bindings, modify `bindings.commands` instead. |<>|This setting can be used to map keys to other keys. @@ -285,6 +286,23 @@ Valid values: Default: empty +[[backend]] +=== backend +The backend to use to display websites. +qutebrowser supports two different web rendering engines / backends, QtWebKit and QtWebEngine. +QtWebKit is based on WebKit (similar to Safari). It was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork. +QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium project. It's slightly more resource hungry that QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice. + +Type: <> + +Valid values: + + * +auto+: Automatically select either QtWebEngine or QtWebKit + * +webkit+: Force QtWebKit + * +webengine+: Force QtWebEngine + +Default: +pass:[auto]+ + [[bindings.commands]] === bindings.commands Keybindings mapping keys to commands in different modes. diff --git a/doc/install.asciidoc b/doc/install.asciidoc index e8ce36c48..a9a1487f1 100644 --- a/doc/install.asciidoc +++ b/doc/install.asciidoc @@ -3,39 +3,50 @@ Installing qutebrowser toc::[] +NOTE: qutebrowser recently had some bigger dependency changes for v1.0.0, which +means those instructions might be out of date in some places. +https://github.com/qutebrowser/qutebrowser/blob/master/doc/contributing.asciidoc[Please help] +updating them if you notice something being broken! + On Debian / Ubuntu ------------------ -qutebrowser should run on these systems: +How to install qutebrowser depends a lot on the version of Debian/Ubuntu you're +running. -* Debian jessie or newer -* Ubuntu Trusty (14.04 LTS) or newer -* Any other distribution based on these (e.g. Linux Mint 17+) +Debian Jessie / Ubuntu 14.04 LTS / Linux Mint < 18 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Unfortunately there is no Debian package in the official repos yet, but installing qutebrowser is -still relatively easy! +Those distributions only have Python 3.4 and a too old Qt version available, +while qutebrowser requires Python 3.5 and Qt 5.7.1 or newer. -You can use packages that are built for every release or build it yourself from git. +It should be possible to install Python 3.5 e.g. from the +https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or via_ipca +https://github.com/pyenv/pyenv[pyenv], but nobody tried that yet. -On Ubuntu 16.04 and 16.10 it's recommended to <> -instead in order to be able to use the new QtWebEngine backend. Newer versions -have a QtWebEngine package in the repositories. +If you get qutebrowser running on those distributions, please +https://github.com/qutebrowser/qutebrowser/blob/master/doc/contributing.asciidoc[contribute] +to update this documentation! -Using the packages -~~~~~~~~~~~~~~~~~~ +Ubuntu 16.04 LTS / Linux Mint 18 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or +QtWebEngine). However, it comes with Python 3.5, so you can +<>. + +Debian Stretch / Ubuntu 17.04 and newer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Those versions come with QtWebEngine in the repositories. This makes it possible +to install qutebrowser via the Debian package. Install the dependencies via apt-get: ---- -# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml python3-pyqt5.qtsql libqt5sql5-sqlite +# apt install python-tox python3-{lxml,pyqt5,sip,jinja2,pygments,yaml} python3-pyqt5.qt{webengine,quick,opengl,sql} libqt5sql5-sqlite ---- -On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to use the -newer QtWebEngine backend. - -To do so, install `python3-pyqt5.qtwebengine` and `python3-pyqt5.qtopengl`, then -start qutebrowser with `--backend webengine`. - Get the qutebrowser package from the https://github.com/qutebrowser/qutebrowser/releases[release page] and download the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package]. @@ -47,35 +58,27 @@ Install the packages: # dpkg -i qutebrowser_*_all.deb ---- -Build it from git -~~~~~~~~~~~~~~~~~ - -Install the dependencies via apt-get: +Some additional hints: +- Alternatively, you can <> to get a newer + QtWebEngine version. +- If running from git, run the following to generate the documentation for the + `:help` command: ++ ---- -# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev python3-pyqt5.qtsql libqt5sql5-sqlite ----- - -On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to install -`python3-pyqt5.qtwebengine` and start qutebrowser with `--backend webengine` in -order to use the new backend. - -To generate the documentation for the `:help` command, when using the git -repository (rather than a release): - ----- -# apt-get install asciidoc source-highlight +# apt-get install --no-install-recommends asciidoc source-highlight $ python3 scripts/asciidoc2html.py ---- -If video or sound don't seem to work, try installing the gstreamer plugins: - +- If you prefer using QtWebKit, there's an up-to-date version available in + Debian experimental, or from http://repo.paretje.be/unstable/[this repository] + for Debian Stretch. +- If video or sound don't work with QtWebKit, try installing the gstreamer plugins: ++ ---- # apt-get install gstreamer1.0-plugins-{bad,base,good,ugly} ---- -Then <>. - On Fedora --------- @@ -116,7 +119,7 @@ $ rm -r qutebrowser-git or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`. -If video or sound don't seem to work, try installing the gstreamer plugins: +If video or sound don't work with QtWebKit, try installing the gstreamer plugins: ---- # pacman -S gst-plugins-{base,good,bad,ugly} gst-libav @@ -125,6 +128,8 @@ If video or sound don't seem to work, try installing the gstreamer plugins: On Gentoo --------- +WARNING: The Gentoo packages (even the live version) are lagging behind a lot, which means those instructions probably won't work anymore. Until things are looking better, it's recommended to <>. + A version of qutebrowser is available in the main repository and can be installed with: ---- @@ -161,15 +166,11 @@ To update to the last Live version, remember to do To include qutebrowser among the updates. -Make sure you have `python3_4` in your `PYTHON_TARGETS` -(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if -necessary. +You'll also need to install `dev-qt/qtwebengine` or a newer QtWebKit using +https://gist.github.com/annulen/309569fb61e5d64a703c055c1e726f71[this ebuild]. -It's also recommended to install QtWebKit-NG via -https://gist.github.com/annulen/309569fb61e5d64a703c055c1e726f71[this ebuild], -or install Qt >= 5.7.1 with QtWebEngine in order to use an up-to-date backend. - -If video or sound don't seem to work, try installing the gstreamer plugins: +If video or sound don't work with QtWebKit, try installing the gstreamer +plugins: ---- # emerge -av gst-plugins-{base,good,bad,ugly,libav} @@ -214,6 +215,8 @@ On openSUSE There are prebuilt RPMs available at https://software.opensuse.org/download.html?project=network&package=qutebrowser[OBS]. +To use the QtWebEngine backend, install `libqt5-qtwebengine`. + On OpenBSD ---------- diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 58744e75d..fc70427f6 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -84,9 +84,6 @@ show it. *--force-color*:: Force colored logging -*--harfbuzz* '{old,new,system,auto}':: - HarfBuzz engine version to use. Default: auto. - *--relaxed-config*:: Silently remove unknown config options. diff --git a/pytest.ini b/pytest.ini index 5401eaacc..d47c173f7 100644 --- a/pytest.ini +++ b/pytest.ini @@ -19,15 +19,12 @@ markers = qtwebengine_todo: Features still missing with QtWebEngine qtwebengine_skip: Tests not applicable with QtWebEngine qtwebkit_skip: Tests not applicable with QtWebKit - qtwebkit_ng_xfail: Tests failing with QtWebKit-NG - qtwebkit_ng_skip: Tests skipped with QtWebKit-NG qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine js_prompt: Tests needing to display a javascript prompt this: Used to mark tests during development no_invalid_lines: Don't fail on unparseable lines in end2end tests issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478 - qt55: Tests only running on Qt 5.5 or later qt_log_level_fail = WARNING qt_log_ignore = ^SpellCheck: .* diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 257e15b0d..5bca765eb 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -52,11 +52,14 @@ from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.keyinput import macros from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.misc import (readline, ipc, savemanager, sessions, - crashsignal, earlyinit, objects, sql, cmdhistory) -from qutebrowser.misc import utilcmds # pylint: disable=unused-import -from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, - objreg, usertypes, standarddir, error) -# We import utilcmds to run the cmdutils.register decorators. + crashsignal, earlyinit, sql, cmdhistory) +from qutebrowser.utils import (log, version, message, utils, urlutils, objreg, + usertypes, standarddir, error) +# pylint: disable=unused-import +# We import those to run the cmdutils.register decorators. +from qutebrowser.mainwindow.statusbar import command +from qutebrowser.misc import utilcmds +# pylint: enable=unused-import qApp = None @@ -74,7 +77,7 @@ def run(args): standarddir.init(args) log.init.debug("Initializing config...") - config.early_init() + config.early_init(args) global qApp qApp = Application(args) @@ -329,17 +332,6 @@ def _open_special_pages(args): tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') - # Legacy QtWebKit warning - - needs_warning = (objects.backend == usertypes.Backend.QtWebKit and - not qtutils.is_qtwebkit_ng()) - warning_shown = general_sect.get('backend-warning-shown') == '1' - - if not warning_shown and needs_warning: - tabbed_browser.tabopen(QUrl('qute://backend-warning'), - background=False) - general_sect['backend-warning-shown'] = '1' - # Quickstart page quickstart_done = general_sect.get('quickstart-done') == '1' diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 79a8502cd..5bb07f129 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -24,6 +24,7 @@ import sys import os.path import shlex import functools +import typing from PyQt5.QtWidgets import QApplication, QTabBar, QDialog from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery @@ -39,10 +40,11 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, - objreg, utils, typing, debug) + objreg, utils, debug) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor, guiprocess from qutebrowser.completion.models import urlmodel, miscmodels +from qutebrowser.mainwindow import mainwindow class CommandDispatcher: @@ -70,7 +72,6 @@ class CommandDispatcher: def _new_tabbed_browser(self, private): """Get a tabbed-browser from a new window.""" - from qutebrowser.mainwindow import mainwindow new_window = mainwindow.MainWindow(private=private) new_window.show() return new_window.tabbed_browser diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index dcb578072..2278a2fa7 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -24,6 +24,7 @@ import posixpath from qutebrowser.browser import webelem from qutebrowser.config import config from qutebrowser.utils import objreg, urlutils, log, message, qtutils +from qutebrowser.mainwindow import mainwindow class Error(Exception): @@ -134,7 +135,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False, window=win_id) if window: - from qutebrowser.mainwindow import mainwindow new_window = mainwindow.MainWindow( private=cur_tabbed_browser.private) new_window.show() diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 478769a85..6bf27e7d8 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -28,7 +28,6 @@ import json import os import time import urllib.parse -import datetime import textwrap import pkg_resources @@ -37,7 +36,7 @@ from PyQt5.QtCore import QUrlQuery, QUrl import qutebrowser from qutebrowser.config import config, configdata, configexc, configdiff from qutebrowser.utils import (version, utils, jinja, log, message, docutils, - objreg, usertypes, qtutils) + objreg) from qutebrowser.misc import objects @@ -224,50 +223,13 @@ def qute_history(url): return 'text/html', json.dumps(history_data(start_time, offset)) else: - if ( - config.val.content.javascript.enabled and - (objects.backend == usertypes.Backend.QtWebEngine or - qtutils.is_qtwebkit_ng()) - ): - return 'text/html', jinja.render( - 'history.html', - title='History', - gap_interval=config.val.history_gap_interval - ) - else: - # Get current date from query parameter, if not given choose today. - curr_date = datetime.date.today() - try: - query_date = QUrlQuery(url).queryItemValue("date") - if query_date: - curr_date = datetime.datetime.strptime(query_date, - "%Y-%m-%d").date() - except ValueError: - log.misc.debug("Invalid date passed to qute:history: " + - query_date) - - one_day = datetime.timedelta(days=1) - next_date = curr_date + one_day - prev_date = curr_date - one_day - - # start_time is the last second of curr_date - start_time = time.mktime(next_date.timetuple()) - 1 - history = [ - (i["url"], i["title"], - datetime.datetime.fromtimestamp(i["time"]), - QUrl(i["url"]).host()) - for i in history_data(start_time) - ] - - return 'text/html', jinja.render( - 'history_nojs.html', - title='History', - history=history, - curr_date=curr_date, - next_date=next_date, - prev_date=prev_date, - today=datetime.date.today(), - ) + if not config.val.content.javascript.enabled: + return 'text/plain', b'JavaScript is required for qute://history' + return 'text/html', jinja.render( + 'history.html', + title='History', + gap_interval=config.val.history_gap_interval + ) @add_handler('javascript') diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index cfa1e98da..ab1b6ad9f 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -23,6 +23,7 @@ import html from qutebrowser.config import config from qutebrowser.utils import usertypes, message, log, objreg, jinja +from qutebrowser.mainwindow import mainwindow class CallSuper(Exception): @@ -234,7 +235,6 @@ def get_tab(win_id, target): elif target == usertypes.ClickTarget.window: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - from qutebrowser.mainwindow import mainwindow window = mainwindow.MainWindow(private=tabbed_browser.private) window.show() win_id = window.win_id diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index df3001b52..85efebd03 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -31,6 +31,7 @@ from PyQt5.QtGui import QMouseEvent from qutebrowser.config import config from qutebrowser.keyinput import modeman +from qutebrowser.mainwindow import mainwindow from qutebrowser.utils import log, usertypes, utils, qtutils, objreg @@ -372,7 +373,6 @@ class AbstractWebElement(collections.abc.MutableMapping): background = click_target == usertypes.ClickTarget.tab_bg tabbed_browser.tabopen(url, background=background) elif click_target == usertypes.ClickTarget.window: - from qutebrowser.mainwindow import mainwindow window = mainwindow.MainWindow(private=tabbed_browser.private) window.show() window.tabbed_browser.tabopen(url) diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 7ed232a49..28ce2f693 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -49,7 +49,6 @@ class DiskCache(QNetworkDiskCache): if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 - if (qtutils.version_check('5.7.1') and - not qtutils.version_check('5.9')): # pragma: no cover + if not qtutils.version_check('5.9'): # pragma: no cover size = 0 self.setMaximumCacheSize(size) diff --git a/qutebrowser/browser/webkit/certificateerror.py b/qutebrowser/browser/webkit/certificateerror.py index 1d2df1562..23353f268 100644 --- a/qutebrowser/browser/webkit/certificateerror.py +++ b/qutebrowser/browser/webkit/certificateerror.py @@ -38,12 +38,7 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper): string=str(self)) def __hash__(self): - try: - # Qt >= 5.4 - return hash(self._error) - except TypeError: - return hash((self._error.certificate().toDer(), - self._error.error())) + return hash(self._error) def __eq__(self, other): return self._error == other._error # pylint: disable=protected-access diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index df7b589c3..770cb2f20 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -24,13 +24,12 @@ import collections import netrc import html -from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, - QUrl, QByteArray) +from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, + QByteArray) from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from qutebrowser.config import config -from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, - urlutils) +from qutebrowser.utils import message, log, usertypes, utils, objreg, urlutils from qutebrowser.browser import shared from qutebrowser.browser.webkit import certificateerror from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, @@ -88,15 +87,9 @@ def _is_secure_cipher(cipher): def init(): """Disable insecure SSL ciphers on old Qt versions.""" - if qtutils.version_check('5.3.0'): - default_ciphers = QSslSocket.defaultCiphers() - log.init.debug("Default Qt ciphers: {}".format( - ', '.join(c.name() for c in default_ciphers))) - else: - # https://codereview.qt-project.org/#/c/75943/ - default_ciphers = QSslSocket.supportedCiphers() - log.init.debug("Supported Qt ciphers: {}".format( - ', '.join(c.name() for c in default_ciphers))) + default_ciphers = QSslSocket.defaultCiphers() + log.init.debug("Default Qt ciphers: {}".format( + ', '.join(c.name() for c in default_ciphers))) good_ciphers = [] bad_ciphers = [] @@ -409,24 +402,11 @@ class NetworkManager(QNetworkAccessManager): tab = objreg.get('tab', scope='tab', window=self._win_id, tab=self._tab_id) current_url = tab.url() - except (KeyError, RuntimeError, TypeError): + except (KeyError, RuntimeError): # https://github.com/qutebrowser/qutebrowser/issues/889 - # Catching RuntimeError and TypeError because we could be in - # the middle of the webpage shutdown here. + # Catching RuntimeError because we could be in the middle of + # the webpage shutdown here. current_url = QUrl() self.set_referer(req, current_url) - - if PYQT_VERSION < 0x050301: - # WORKAROUND (remove this when we bump the requirements to 5.3.1) - # - # If we don't disable our message handler, we get a freeze if a - # warning is printed due to a PyQt bug, e.g. when clicking a - # currency on http://ch.mouser.com/localsites/ - # - # See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034420.html - with log.disable_qt_msghandler(): - reply = super().createRequest(op, req, outgoing_data) - else: - reply = super().createRequest(op, req, outgoing_data) - return reply + return super().createRequest(op, req, outgoing_data) diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py index 19e4ef15c..d595a6e95 100644 --- a/qutebrowser/browser/webkit/tabhistory.py +++ b/qutebrowser/browser/webkit/tabhistory.py @@ -25,13 +25,7 @@ from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl from qutebrowser.utils import qtutils -def _encode_url(url): - """Encode a QUrl suitable to pass to QWebHistory.""" - data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*')) - return data.decode('ascii') - - -def _serialize_ng(items, current_idx, stream): +def _serialize_items(items, current_idx, stream): # {'currentItemIndex': 0, # 'history': [{'children': [], # 'documentSequenceNumber': 1485030525573123, @@ -47,13 +41,13 @@ def _serialize_ng(items, current_idx, stream): # 'urlString': 'about:blank'}]} data = {'currentItemIndex': current_idx, 'history': []} for item in items: - data['history'].append(_serialize_item_ng(item)) + data['history'].append(_serialize_item(item)) stream.writeInt(3) # history stream version stream.writeQVariantMap(data) -def _serialize_item_ng(item): +def _serialize_item(item): data = { 'originalURLString': item.original_url.toString(QUrl.FullyEncoded), 'scrollPosition': {'x': 0, 'y': 0}, @@ -68,82 +62,6 @@ def _serialize_item_ng(item): return data -def _serialize_old(items, current_idx, stream): - ### Source/WebKit/qt/Api/qwebhistory.cpp operator<< - stream.writeInt(2) # history stream version - stream.writeInt(len(items)) - stream.writeInt(current_idx) - - for i, item in enumerate(items): - _serialize_item_old(i, item, stream) - - -def _serialize_item_old(i, item, stream): - """Serialize a single WebHistoryItem into a QDataStream. - - Args: - i: The index of the current item. - item: The WebHistoryItem to write. - stream: The QDataStream to write to. - """ - ### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState - ## urlString - stream.writeQString(_encode_url(item.url)) - ## title - stream.writeQString(item.title) - ## originalURLString - stream.writeQString(_encode_url(item.original_url)) - - ### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree - ## backForwardTreeEncodingVersion - stream.writeUInt32(2) - ## size (recursion stack) - stream.writeUInt64(0) - ## node->m_documentSequenceNumber - # If two HistoryItems have the same document sequence number, then they - # refer to the same instance of a document. Traversing history from one - # such HistoryItem to another preserves the document. - stream.writeInt64(i + 1) - ## size (node->m_documentState) - stream.writeUInt64(0) - ## node->m_formContentType - # info used to repost form data - stream.writeQString(None) - ## hasFormData - stream.writeBool(False) - ## node->m_itemSequenceNumber - # If two HistoryItems have the same item sequence number, then they are - # clones of one another. Traversing history from one such HistoryItem to - # another is a no-op. HistoryItem clones are created for parent and - # sibling frames when only a subframe navigates. - stream.writeInt64(i + 1) - ## node->m_referrer - stream.writeQString(None) - ## node->m_scrollPoint (x) - try: - stream.writeInt32(item.user_data['scroll-pos'].x()) - except (KeyError, TypeError): - stream.writeInt32(0) - ## node->m_scrollPoint (y) - try: - stream.writeInt32(item.user_data['scroll-pos'].y()) - except (KeyError, TypeError): - stream.writeInt32(0) - ## node->m_pageScaleFactor - stream.writeFloat(1) - ## hasStateObject - # Support for HTML5 History - stream.writeBool(False) - ## node->m_target - stream.writeQString(None) - - ### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState - ## validUserData - # We could restore the user data here, but we prefer to use the - # QWebHistoryItem API for that. - stream.writeBool(False) - - def serialize(items): """Serialize a list of QWebHistoryItems to a data stream. @@ -180,10 +98,7 @@ def serialize(items): else: current_idx = 0 - if qtutils.is_qtwebkit_ng(): - _serialize_ng(items, current_idx, stream) - else: - _serialize_old(items, current_idx, stream) + _serialize_items(items, current_idx, stream) user_data += [item.user_data for item in items] diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 216da2dd9..54dd6519d 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -292,9 +292,6 @@ class WebKitElement(webelem.AbstractWebElement): elem = elem._parent() # pylint: disable=protected-access def _move_text_cursor(self): - if self is None: - # old PyQt versions call the slot after the element is deleted. - return if self.is_text_input() and self.is_editable(): self._tab.caret.move_to_end_of_document() diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 53b26431c..3354a9486 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -33,7 +33,7 @@ from PyQt5.QtGui import QFont from PyQt5.QtWebKit import QWebSettings from qutebrowser.config import config, websettings -from qutebrowser.utils import standarddir, urlutils, qtutils +from qutebrowser.utils import standarddir, urlutils from qutebrowser.browser import shared @@ -131,13 +131,6 @@ def init(_args): QWebSettings.setOfflineStoragePath( os.path.join(data_path, 'offline-storage')) - if (config.val.content.private_browsing and - not qtutils.version_check('5.4.2')): - # WORKAROUND for https://codereview.qt-project.org/#/c/108936/ - # Won't work when private browsing is not enabled globally, but that's - # the best we can do... - QWebSettings.setIconDatabasePath('') - websettings.init_mappings(MAPPINGS) _set_user_stylesheet() config.instance.changed.connect(_update_settings) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index b81599e8a..cf2156431 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -55,20 +55,14 @@ class WebKitPrinting(browsertab.AbstractPrinting): """QtWebKit implementations related to printing.""" - def _do_check(self): - if not qtutils.check_print_compat(): - # WORKAROUND (remove this when we bump the requirements to 5.3.0) - raise browsertab.WebTabError( - "Printing on Qt < 5.3.0 on Windows is broken, please upgrade!") - def check_pdf_support(self): - self._do_check() + pass def check_printer_support(self): - self._do_check() + pass def check_preview_support(self): - self._do_check() + pass def to_pdf(self, filename): printer = QPrinter() diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index c20952593..7e1d991b9 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -22,7 +22,7 @@ import html import functools -from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint from PyQt5.QtGui import QDesktopServices from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from PyQt5.QtWidgets import QFileDialog @@ -33,8 +33,8 @@ from qutebrowser.config import config from qutebrowser.browser import pdfjs, shared from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit.network import networkmanager -from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils, - objreg, debug, urlutils) +from qutebrowser.utils import (message, usertypes, log, jinja, objreg, debug, + urlutils) class BrowserPage(QWebPage): @@ -87,22 +87,16 @@ class BrowserPage(QWebPage): self.restoreFrameStateRequested.connect( self.on_restore_frame_state_requested) - if PYQT_VERSION > 0x050300: - # WORKAROUND (remove this when we bump the requirements to 5.3.1) - # We can't override javaScriptPrompt with older PyQt-versions because - # of a bug in PyQt. - # See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html - - def javaScriptPrompt(self, frame, js_msg, default): - """Override javaScriptPrompt to use qutebrowser prompts.""" - if self._is_shutting_down: - return (False, "") - try: - return shared.javascript_prompt(frame.url(), js_msg, default, - abort_on=[self.loadStarted, - self.shutting_down]) - except shared.CallSuper: - return super().javaScriptPrompt(frame, js_msg, default) + def javaScriptPrompt(self, frame, js_msg, default): + """Override javaScriptPrompt to use qutebrowser prompts.""" + if self._is_shutting_down: + return (False, "") + try: + return shared.javascript_prompt(frame.url(), js_msg, default, + abort_on=[self.loadStarted, + self.shutting_down]) + except shared.CallSuper: + return super().javaScriptPrompt(frame, js_msg, default) def _handle_errorpage(self, info, errpage): """Display an error page if needed. @@ -225,10 +219,6 @@ class BrowserPage(QWebPage): def on_print_requested(self, frame): """Handle printing when requested via javascript.""" - if not qtutils.check_print_compat(): - message.error("Printing on Qt < 5.3.0 on Windows is broken, " - "please upgrade!") - return printdiag = QPrintDialog() printdiag.setAttribute(Qt.WA_DeleteOnClose) printdiag.open(lambda: frame.print(printdiag.printer())) @@ -350,15 +340,7 @@ class BrowserPage(QWebPage): frame: The QWebFrame which gets saved. item: The QWebHistoryItem to be saved. """ - try: - if frame != self.mainFrame(): - return - except RuntimeError: - # With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab: - # RuntimeError: wrapped C/C++ object of type BrowserPage has - # been deleted - # Since the information here isn't that important for closing web - # views anyways, we ignore this error. + if frame != self.mainFrame(): return data = { 'zoom': frame.zoomFactor(), @@ -401,9 +383,6 @@ class BrowserPage(QWebPage): """ return ext in self._extension_handlers - # WORKAROUND for: - # http://www.riverbankcomputing.com/pipermail/pyqt/2014-August/034722.html - @utils.prevent_exceptions(False, PYQT_VERSION < 0x50302) def extension(self, ext, opt, out): """Override QWebPage::extension to provide error pages. diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 95e8c14f0..5608995a4 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -29,7 +29,7 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage from qutebrowser.config import config from qutebrowser.keyinput import modeman -from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, debug +from qutebrowser.utils import log, usertypes, utils, objreg, debug from qutebrowser.browser.webkit import webpage @@ -57,7 +57,7 @@ class WebView(QWebView): def __init__(self, *, win_id, tab_id, tab, private, parent=None): super().__init__(parent) - if sys.platform == 'darwin' and qtutils.version_check('5.4'): + if sys.platform == 'darwin': # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 # See https://github.com/qutebrowser/qutebrowser/issues/462 self.setStyle(QStyleFactory.create('Fusion')) @@ -74,13 +74,9 @@ class WebView(QWebView): page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id, tabdata=tab.data, private=private, parent=self) - - try: - page.setVisibilityState( - QWebPage.VisibilityStateVisible if self.isVisible() - else QWebPage.VisibilityStateHidden) - except AttributeError: - pass + page.setVisibilityState( + QWebPage.VisibilityStateVisible if self.isVisible() + else QWebPage.VisibilityStateHidden) self.setPage(page) @@ -240,12 +236,8 @@ class WebView(QWebView): Return: The superclass event return value. """ - try: - self.page().setVisibilityState(QWebPage.VisibilityStateVisible) - except AttributeError: - pass - super().showEvent(e) + self.page().setVisibilityState(QWebPage.VisibilityStateVisible) def hideEvent(self, e): """Extend hideEvent to set the page visibility state to hidden. @@ -256,12 +248,8 @@ class WebView(QWebView): Return: The superclass event return value. """ - try: - self.page().setVisibilityState(QWebPage.VisibilityStateHidden) - except AttributeError: - pass - super().hideEvent(e) + self.page().setVisibilityState(QWebPage.VisibilityStateHidden) def mousePressEvent(self, e): """Set the tabdata ClickTarget on a mousepress. diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 148be8393..233f9d1a7 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -22,10 +22,11 @@ import inspect import collections import traceback +import typing from qutebrowser.commands import cmdexc, argparser from qutebrowser.utils import (log, utils, message, docutils, objreg, - usertypes, typing) + usertypes) from qutebrowser.utils import debug as debug_utils from qutebrowser.misc import objects @@ -415,10 +416,7 @@ class Command: # We also can't use isinstance here because typing.Union doesn't # support that. # pylint: disable=no-member,useless-suppression - try: - types = list(typ.__union_params__) - except AttributeError: - types = list(typ.__args__) + types = list(typ.__args__) # pylint: enable=no-member,useless-suppression if param.default is not inspect.Parameter.empty: types.append(type(param.default)) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 4ccadfefb..678417453 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -28,9 +28,10 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl from PyQt5.QtWidgets import QMessageBox from qutebrowser.config import configdata, configexc, configtypes, configfiles -from qutebrowser.utils import utils, objreg, message, log, usertypes -from qutebrowser.misc import objects, msgbox -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.utils import (utils, objreg, message, log, usertypes, jinja, + qtutils) +from qutebrowser.misc import objects, msgbox, earlyinit +from qutebrowser.commands import cmdexc, cmdutils, runners from qutebrowser.completion.models import configmodel # An easy way to access the config from other code via config.val.foo @@ -176,8 +177,6 @@ class KeyConfig: def bind(self, key, command, *, mode, force=False, save_yaml=False): """Add a new binding from key to command.""" - # Doing this here to work around a Python 3.4 circular import - from qutebrowser.commands import runners key = self._prepare(key, mode) parser = runners.CommandParser() @@ -395,7 +394,7 @@ class Config(QObject): def _set_value(self, opt, value): """Set the given option to the given value.""" - if objects.backend is not None: + if not isinstance(objects.backend, objects.NoBackend): if objects.backend not in opt.backends: raise configexc.BackendError(objects.backend) @@ -594,8 +593,6 @@ def set_register_stylesheet(obj, *, stylesheet=None, update=True): @functools.lru_cache() def _render_stylesheet(stylesheet): """Render the given stylesheet jinja template.""" - # Imported here to avoid a Python 3.4 circular import - from qutebrowser.utils import jinja with jinja.environment.no_autoescape(): template = jinja.environment.from_string(stylesheet) return template.render(conf=val) @@ -645,7 +642,7 @@ class StyleSheetObserver(QObject): instance.changed.connect(self._update_stylesheet) -def early_init(): +def early_init(args): """Initialize the part of the config which works without a QApplication.""" configdata.init() @@ -690,6 +687,33 @@ def early_init(): configfiles.init() + objects.backend = get_backend(args) + earlyinit.init_with_backend(objects.backend) + + +def get_backend(args): + """Find out what backend to use based on available libraries.""" + try: + import PyQt5.QtWebKit # pylint: disable=unused-variable + except ImportError: + webkit_available = False + else: + webkit_available = qtutils.is_new_qtwebkit() + + str_to_backend = { + 'webkit': usertypes.Backend.QtWebKit, + 'webengine': usertypes.Backend.QtWebEngine, + } + + if args.backend is not None: + return str_to_backend[args.backend] + elif val.backend != 'auto': + return str_to_backend[val.backend] + elif webkit_available: + return usertypes.Backend.QtWebKit + else: + return usertypes.Backend.QtWebEngine + def late_init(save_manager): """Initialize the rest of the config after the QApplication is created.""" diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 1ff02c3b8..8504c224e 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -101,6 +101,28 @@ qt_args: https://peter.sh/experiments/chromium-command-line-switches/ for a list) will work. +backend: + type: + name: String + valid_values: + - auto: Automatically select either QtWebEngine or QtWebKit + - webkit: Force QtWebKit + - webengine: Force QtWebEngine + default: auto + desc: >- + The backend to use to display websites. + + qutebrowser supports two different web rendering engines / backends, + QtWebKit and QtWebEngine. + + QtWebKit is based on WebKit (similar to Safari). It was discontinued by the + Qt project with Qt 5.6, but picked up as a well maintained fork: + https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork. + + QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium + project. It's slightly more resource hungry that QtWebKit and has a couple + of missing features in qutebrowser, but is generally the preferred choice. + ## auto_save auto_save.interval: diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index dd0da5401..a4514cd1f 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -19,7 +19,7 @@ """Exceptions related to config parsing.""" -from qutebrowser.utils import utils +from qutebrowser.utils import utils, jinja class Error(Exception): @@ -108,7 +108,6 @@ class ConfigFileErrors(Error): def to_html(self): """Get the error texts as a HTML snippet.""" - from qutebrowser.utils import jinja template = jinja.environment.from_string(""" Errors occurred while reading {{ basename }}: diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 093ad4aee..3810bca8c 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -30,7 +30,7 @@ import yaml from PyQt5.QtCore import QSettings import qutebrowser -from qutebrowser.config import configexc +from qutebrowser.config import configexc, config from qutebrowser.utils import standarddir, utils, qtutils @@ -51,8 +51,10 @@ class StateConfig(configparser.ConfigParser): self.add_section(sect) except configparser.DuplicateSectionError: pass - # See commit a98060e020a4ba83b663813a4b9404edb47f28ad. - self['general'].pop('fooled', None) + + deleted_keys = ['fooled', 'backend-warning-shown'] + for key in deleted_keys: + self['general'].pop(key, None) def init_save_manager(self, save_manager): """Make sure the config gets saved properly. @@ -152,8 +154,8 @@ class ConfigAPI: errors: Errors which occurred while setting options. """ - def __init__(self, config, keyconfig): - self._config = config + def __init__(self, conf, keyconfig): + self._config = conf self._keyconfig = keyconfig self.load_autoconfig = True self.errors = [] @@ -189,7 +191,6 @@ class ConfigAPI: def read_config_py(filename=None): """Read a config.py file.""" - from qutebrowser.config import config api = ConfigAPI(config.instance, config.key_instance) if filename is None: diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index b8c9c1e05..b92722d16 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -59,9 +59,9 @@ from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import QTabWidget, QTabBar -from qutebrowser.commands import cmdutils +from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.config import configexc -from qutebrowser.utils import standarddir, utils, qtutils +from qutebrowser.utils import standarddir, utils, qtutils, urlutils SYSTEM_PROXY = object() # Return value for Proxy type @@ -791,7 +791,6 @@ class Command(BaseType): if not Command.unvalidated: Command.unvalidated = True try: - from qutebrowser.commands import runners, cmdexc parser = runners.CommandParser() try: parser.parse_all(value) @@ -1287,7 +1286,6 @@ class Proxy(BaseType): ('none', "Don't use any proxy")) def to_py(self, value): - from qutebrowser.utils import urlutils self._basic_py_validation(value, str) if not value: return None @@ -1352,7 +1350,6 @@ class FuzzyUrl(BaseType): """A URL which gets interpreted as search if needed.""" def to_py(self, value): - from qutebrowser.utils import urlutils self._basic_py_validation(value, str) if not value: return None diff --git a/qutebrowser/html/backend-warning.html b/qutebrowser/html/backend-warning.html deleted file mode 100644 index 0ba8e95ee..000000000 --- a/qutebrowser/html/backend-warning.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends "styled.html" %} - -{% block style %} -{{super()}} -.note { - font-size: smaller; - color: grey; -} - -.mono { - font-family: monospace; -} -{% endblock %} - -{% block content %} -

Legacy QtWebKit backend

- -Note this warning will only appear once. Use :open -qute://backend-warning to show it again at a later time. - -

- You're using qutebrowser with the legacy QtWebKit backend. It's still the - default until a few remaining issues are sorted out. If you can, it's - strongly suggested to switch earlier, as legacy QtWebKit has known security - issues and also breaks things on various websites. -

- -

Using QtWebEngine instead

- -This is usually the better choice if you aren't using Nouveau graphics, and -don't need any features which are currently unavailable with QtWebEngine (like -the qute://settings page or caret browsing). - -{% macro install_webengine(package) -%} - You should be able to install {{ package }} and start qutebrowser with --backend webengine to use the new backend. -{%- endmacro %} - -{% macro please_open_issue() -%} - If you know more, please open an issue! -{%- endmacro %} - -{% macro unknown_system() -%} - There's no information available for your system. {{ please_open_issue() }} -{%- endmacro %} - -

-{% if distribution.parsed == Distribution.ubuntu %} - {% if distribution.version == none %} - {{ unknown_system() }} - {% elif distribution.version >= version('17.04') %} - {{ install_webengine('python3-pyqt5.qtwebengine') }} - {% elif distribution.version >= version('16.04') %} - QtWebEngine is only available in Ubuntu's repositories since 17.04, but you can install qutebrowser via tox with tox -e mkvenv-pypi to use the new backend. - {% else %} - Unfortunately, no easy way is known to install QtWebEngine on Ubuntu < 16.04. {{ please_open_issue() }} - {% endif %} -{% elif distribution.parsed == Distribution.debian %} - {% if distribution.version == none %} - {{ unknown_system() }} - {% elif distribution.version >= version('9') %} - {{ install_webengine('python3-pyqt5.qtwebengine') }} - {% else %} - Unfortunately, no easy way is known to install QtWebEngine on Debian < 9. {{ please_open_issue() }} - {% endif %} -{% elif distribution.parsed in [Distribution.arch, Distribution.manjaro] %} - {{ install_webengine('qt5-webengine') }} -{% elif distribution.parsed == Distribution.void %} - {{ install_webengine('python-PyQt5-webengine') }} -{% elif distribution.parsed == Distribution.fedora %} - {{ install_webengine('qt5-qtwebengine') }} -{% elif distribution.parsed == Distribution.opensuse %} - {{ install_webengine('libqt5-qtwebengine') }} -{% elif distribution.parsed == Distribution.gentoo %} - {{ install_webengine('dev-qt/qtwebengine') }} -{% else %} - {{ unknown_system() }} -{% endif %} -

- -

Using QtWebKit-NG instead

- -This is a drop-in replacement for legacy QtWebKit. - -

-{% if distribution.parsed == Distribution.debian and distribution.version != none and distribution.version >= version('9') %} - There are unofficial QtWebKit-NG packages available. -{% elif distribution.parsed in [Distribution.ubuntu, Distribution.debian] %} - No easy way is known to install QtWebKit-NG on your system. - There are unofficial QtWebKit-NG packages available, but they are intended for Debian Unstable. - {{ please_open_issue() }} -{% elif distribution.parsed in [Distribution.arch, Distribution.manjaro] %} - With an updated qt5-webkit package, you should already get QtWebKit-NG. -{% elif distribution.parsed == Distribution.gentoo %} - There's an unofficial ebuild available. -{% else %} - {{ unknown_system() }} -{% endif %} -

- -{% endblock %} diff --git a/qutebrowser/html/history_nojs.html b/qutebrowser/html/history_nojs.html deleted file mode 100644 index bcc5663c1..000000000 --- a/qutebrowser/html/history_nojs.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "styled.html" %} - -{% block style %} -{{super()}} -body { - max-width: 1440px; -} - -td.title { - word-break: break-all; -} - -td.time { - color: #555; - text-align: right; - white-space: nowrap; -} - -table { - margin-bottom: 30px; -} - -.date { - color: #555; - font-size: 12pt; - padding-bottom: 15px; - font-weight: bold; - text-align: left; -} - -.pagination-link { - color: #555; - font-weight: bold; - margn-bottom: 15px; - text-decoration: none; -} -{% endblock %} -{% block content %} - -

Browsing history

- - - - - {% for url, title, time, host in history %} - - - - - {% endfor %} - -
{{curr_date.strftime("%a, %d %B %Y")}}
- {{title}} - {{host}} - {{time.strftime("%X")}}
- - -{% if today >= next_date %} - -{% endif %} -{% endblock %} diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 04ae464b3..373a9030a 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -31,8 +31,7 @@ from qutebrowser.commands import runners, cmdutils from qutebrowser.config import config, configfiles from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, jinja, debug) -from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt -from qutebrowser.mainwindow.statusbar import bar +from qutebrowser.mainwindow import messageview, prompt from qutebrowser.completion import completionwidget, completer from qutebrowser.keyinput import modeman from qutebrowser.browser import (commands, downloadview, hints, @@ -140,6 +139,11 @@ class MainWindow(QWidget): parent: The parent the window should get. """ super().__init__(parent) + # Late import to avoid a circular dependency + # - browsertab -> hints -> webelem -> mainwindow -> bar -> browsertab + from qutebrowser.mainwindow import tabbedbrowser + from qutebrowser.mainwindow.statusbar import bar + self.setAttribute(Qt.WA_DeleteOnClose) self._commandrunner = None self._overlays = [] diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py index 7677a796b..c8707e0b1 100644 --- a/qutebrowser/mainwindow/statusbar/progress.py +++ b/qutebrowser/mainwindow/statusbar/progress.py @@ -61,10 +61,6 @@ class Progress(QProgressBar): def on_tab_changed(self, tab): """Set the correct value when the current tab changed.""" - if self is None: # pragma: no branch - # This should never happen, but for some weird reason it does - # sometimes. - return # pragma: no cover self.setValue(tab.progress()) if tab.load_status() == usertypes.LoadStatus.loading: self.show() diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 5c46ec481..acf966cd5 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -28,7 +28,7 @@ from PyQt5.QtGui import QIcon from qutebrowser.config import config from qutebrowser.keyinput import modeman -from qutebrowser.mainwindow import tabwidget +from qutebrowser.mainwindow import tabwidget, mainwindow from qutebrowser.browser import signalfilter, browsertab from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg, urlutils, message, jinja) @@ -432,7 +432,6 @@ class TabbedBrowser(tabwidget.TabWidget): if (config.val.tabs.tabs_are_windows and self.count() > 0 and not ignore_tabs_are_windows): - from qutebrowser.mainwindow import mainwindow window = mainwindow.MainWindow(private=self.private) window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index dbdb89ee4..f86699589 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -70,9 +70,6 @@ class TabWidget(QTabWidget): @config.change_filter('tabs') def _init_config(self): """Initialize attributes based on the config.""" - if self is None: # pragma: no cover - # WORKAROUND for PyQt 5.2 - return tabbar = self.tabBar() self.setMovable(True) self.setTabsClosable(False) @@ -744,24 +741,17 @@ class TabBarStyle(QCommonStyle): log.misc.warning("Could not get layouts for tab!") return QRect() return layouts.text - elif sr == QStyle.SE_TabWidgetTabBar: + elif sr in [QStyle.SE_TabWidgetTabBar, + QStyle.SE_TabBarScrollLeftButton]: + # Handling SE_TabBarScrollLeftButton so the left scroll button is + # aligned properly. Otherwise, empty space will be shown after the + # last tab even though the button width is set to 0 + # # Need to use super() because we also use super() to render # element in drawControl(); otherwise, we may get bit by # style differences... - rct = super().subElementRect(sr, opt, widget) - return rct + return super().subElementRect(sr, opt, widget) else: - try: - # We need this so the left scroll button is aligned properly. - # Otherwise, empty space will be shown after the last tab even - # though the button width is set to 0 - # - # QStyle.SE_TabBarScrollLeftButton was added in Qt 5.7 - if sr == QStyle.SE_TabBarScrollLeftButton: - return super().subElementRect(sr, opt, widget) - except AttributeError: - pass - return self._style.subElementRect(sr, opt, widget) def _tab_layout(self, opt): diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 34183041b..fc3dde673 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -43,12 +43,12 @@ except ImportError: # pragma: no cover # to stderr. def check_python_version(): """Check if correct python version is run.""" - if sys.hexversion < 0x03040000: + if sys.hexversion < 0x03050000: # We don't use .format() and print_function here just in case someone # still has < 2.6 installed. # pylint: disable=bad-builtin version_str = '.'.join(map(str, sys.version_info[:3])) - text = ("At least Python 3.4 is required to run qutebrowser, but " + + text = ("At least Python 3.5 is required to run qutebrowser, but " + version_str + " is installed!\n") if Tk and '--no-err-windows' not in sys.argv: # pragma: no cover root = Tk() diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index a64a2799b..d2fb65ced 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -19,7 +19,7 @@ """Things which need to be done really early (e.g. before importing Qt). -At this point we can be sure we have all python 3.4 features available. +At this point we can be sure we have all python 3.5 features available. """ try: @@ -29,7 +29,6 @@ try: except ImportError: hunter = None -import os import sys import faulthandler import traceback @@ -41,8 +40,6 @@ try: except ImportError: tkinter = None -import pkg_resources - # NOTE: No qutebrowser or PyQt import should be done here, as some early # initialization needs to take place before that! @@ -68,7 +65,7 @@ def _missing_str(name, *, windows=None, pip=None, webengine=False): if not webengine: lines = ['If you installed a qutebrowser package for your ' 'distribution, please report this as a bug.'] - blocks.append('
'.join(lines)) + blocks.append('
'.join(lines)) if windows is not None: lines = ["On Windows:"] lines += windows.splitlines() @@ -140,73 +137,6 @@ def init_faulthandler(fileobj=sys.__stderr__): faulthandler.register(signal.SIGUSR1) -def _qt_version(): - """Get the running Qt version. - - Needs to be in a function so we can do a local import easily (to not import - from QtCore too early) but can patch this out easily for tests. - """ - from PyQt5.QtCore import qVersion - return pkg_resources.parse_version(qVersion()) - - -def fix_harfbuzz(args): - """Fix harfbuzz issues. - - This switches to the most stable harfbuzz font rendering engine available - on the platform instead of using the system wide one. - - This fixes crashes on various sites. - - - On Qt 5.2 (and probably earlier) the new engine probably has more - crashes and is also experimental. - - e.g. https://bugreports.qt.io/browse/QTBUG-36099 - - - On Qt 5.3.0 there's a bug that affects a lot of websites: - https://bugreports.qt.io/browse/QTBUG-39278 - So the new engine will be more stable. - - - On Qt 5.3.1 this bug is fixed and the old engine will be the more stable - one again. - - - On Qt 5.4 the new engine is the default and most bugs are taken care of. - - IMPORTANT: This needs to be done before QWidgets is imported in any way! - - WORKAROUND (remove this when we bump the requirements to 5.3.1) - - Args: - args: The argparse namespace. - """ - from qutebrowser.utils import log - if 'PyQt5.QtWidgets' in sys.modules: - msg = "Harfbuzz fix attempted but QtWidgets is already imported!" - if getattr(sys, 'frozen', False): - log.init.debug(msg) - else: - log.init.warning(msg) - if sys.platform.startswith('linux') and args.harfbuzz == 'auto': - if _qt_version() == pkg_resources.parse_version('5.3.0'): - log.init.debug("Using new harfbuzz engine (auto)") - os.environ['QT_HARFBUZZ'] = 'new' - elif _qt_version() < pkg_resources.parse_version('5.4.0'): - log.init.debug("Using old harfbuzz engine (auto)") - os.environ['QT_HARFBUZZ'] = 'old' - else: - log.init.debug("Using system harfbuzz engine (auto)") - elif args.harfbuzz in ['old', 'new']: - # forced harfbuzz variant - # FIXME looking at the Qt code, 'new' isn't a valid value, but leaving - # it empty and using new yields different behavior... - # (probably irrelevant when workaround gets removed) - log.init.debug("Using {} harfbuzz engine (forced)".format( - args.harfbuzz)) - os.environ['QT_HARFBUZZ'] = args.harfbuzz - else: - log.init.debug("Using system harfbuzz engine") - - def check_pyqt_core(): """Check if PyQt core is installed.""" try: @@ -233,26 +163,6 @@ def check_pyqt_core(): sys.exit(1) -def get_backend(args): - """Find out what backend to use based on available libraries. - - Note this function returns the backend as a string so we don't have to - import qutebrowser.utils.usertypes yet. - """ - try: - import PyQt5.QtWebKit # pylint: disable=unused-variable - webkit_available = True - except ImportError: - webkit_available = False - - if args.backend is not None: - return args.backend - elif webkit_available: - return 'webkit' - else: - return 'webengine' - - def qt_version(qversion=None, qt_version_str=None): """Get a Qt version string based on the runtime/compiled versions.""" if qversion is None: @@ -268,50 +178,65 @@ def qt_version(qversion=None, qt_version_str=None): return qversion -def check_qt_version(backend): +def check_qt_version(): """Check if the Qt version is recent enough.""" from PyQt5.QtCore import PYQT_VERSION, PYQT_VERSION_STR from qutebrowser.utils import qtutils - if (not qtutils.version_check('5.2.0', strict=True) or + if (not qtutils.version_check('5.7.1', strict=True) or PYQT_VERSION < 0x050200): - text = ("Fatal error: Qt and PyQt >= 5.2.0 are required, but Qt {} / " - "PyQt {} is installed.".format(qt_version(), - PYQT_VERSION_STR)) - _die(text) - elif (backend == 'webengine' and ( - not qtutils.version_check('5.7.1', strict=True) or - PYQT_VERSION < 0x050700)): - text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required for " - "QtWebEngine support, but Qt {} / PyQt {} is installed." - .format(qt_version(), PYQT_VERSION_STR)) + text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required, " + "but Qt {} / PyQt {} is installed.".format(qt_version(), + PYQT_VERSION_STR)) _die(text) -def check_ssl_support(backend): +def check_ssl_support(): """Check if SSL support is available.""" - from qutebrowser.utils import log - + # pylint: disable=unused-variable try: from PyQt5.QtNetwork import QSslSocket except ImportError: _die("Fatal error: Your Qt is built without SSL support.") + +def check_backend_ssl_support(backend): + """Check for full SSL availability when we know the backend.""" + from PyQt5.QtNetwork import QSslSocket + from qutebrowser.utils import log, usertypes text = ("Could not initialize QtNetwork SSL support. If you use " "OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux " "or Debian Stretch), you need to set LD_LIBRARY_PATH to the path " - "of OpenSSL 1.0.") - if backend == 'webengine': - text += " This only affects downloads." + "of OpenSSL 1.0. This only affects downloads.") if not QSslSocket.supportsSsl(): - if backend == 'webkit': + if backend == usertypes.Backend.QtWebKit: _die("Could not initialize SSL support.") else: - assert backend == 'webengine' + assert backend == usertypes.Backend.QtWebEngine log.init.warning(text) -def check_libraries(backend): +def _check_modules(modules): + """Make sure the given modules are available.""" + from qutebrowser.utils import log + + for name, text in modules.items(): + try: + # https://github.com/pallets/jinja/pull/628 + # https://bitbucket.org/birkenfeld/pygments-main/issues/1314/ + # https://github.com/pallets/jinja/issues/646 + # https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e + messages = ['invalid escape sequence', + 'Flags not at the start of the expression'] + with log.ignore_py_warnings( + category=DeprecationWarning, + message=r'({})'.format('|'.join(messages))): + importlib.import_module(name) + except ImportError as e: + _die(text, e) + + +def check_libraries(): """Check if all needed Python libraries are installed.""" modules = { 'pkg_resources': @@ -339,32 +264,37 @@ def check_libraries(backend): 'PyQt5.QtQml': _missing_str("PyQt5.QtQml"), 'PyQt5.QtSql': _missing_str("PyQt5.QtSql"), } - if backend == 'webengine': - modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine", - webengine=True) - modules['PyQt5.QtOpenGL'] = _missing_str("PyQt5.QtOpenGL") + _check_modules(modules) + + +def check_backend_libraries(backend): + """Make sure the libraries needed by the given backend are available. + + Args: + backend: The backend as usertypes.Backend member. + """ + from qutebrowser.utils import usertypes + if backend == usertypes.Backend.QtWebEngine: + modules = { + 'PyQt5.QtWebEngineWidgets': + _missing_str("QtWebEngine", webengine=True), + 'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"), + } else: - assert backend == 'webkit' - modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit") - modules['PyQt5.QtWebKitWidgets'] = _missing_str( - "PyQt5.QtWebKitWidgets") + assert backend == usertypes.Backend.QtWebKit, backend + modules = { + 'PyQt5.QtWebKit': _missing_str("PyQt5.QtWebKit"), + 'PyQt5.QtWebKitWidgets': _missing_str("PyQt5.QtWebKitWidgets"), + } + _check_modules(modules) - from qutebrowser.utils import log - for name, text in modules.items(): - try: - # https://github.com/pallets/jinja/pull/628 - # https://bitbucket.org/birkenfeld/pygments-main/issues/1314/ - # https://github.com/pallets/jinja/issues/646 - # https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e - messages = ['invalid escape sequence', - 'Flags not at the start of the expression'] - with log.ignore_py_warnings( - category=DeprecationWarning, - message=r'({})'.format('|'.join(messages))): - importlib.import_module(name) - except ImportError as e: - _die(text, e) +def check_new_webkit(backend): + """Make sure we use QtWebEngine or a new QtWebKit.""" + from qutebrowser.utils import usertypes, qtutils + if backend == usertypes.Backend.QtWebKit and not qtutils.is_new_qtwebkit(): + _die("qutebrowser does not support legacy QtWebKit versions anymore, " + "see the installation docs for details.") def remove_inputhook(): @@ -395,18 +325,7 @@ def check_optimize_flag(): "unexpected behavior may occur.") -def set_backend(backend): - """Set the objects.backend global to the given backend (as string).""" - from qutebrowser.misc import objects - from qutebrowser.utils import usertypes - backends = { - 'webkit': usertypes.Backend.QtWebKit, - 'webengine': usertypes.Backend.QtWebEngine, - } - objects.backend = backends[backend] - - -def earlyinit(args): +def early_init(args): """Do all needed early initialization. Note that it's vital the other earlyinit functions get called in the right @@ -423,15 +342,23 @@ def earlyinit(args): check_pyqt_core() # Init logging as early as possible init_log(args) - # Now the faulthandler is enabled we fix the Qt harfbuzzing library, before - # importing QtWidgets. - fix_harfbuzz(args) # Now we can be sure QtCore is available, so we can print dialogs on # errors, so people only using the GUI notice them as well. - backend = get_backend(args) - check_qt_version(backend) + check_libraries() + check_qt_version() remove_inputhook() - check_libraries(backend) - check_ssl_support(backend) + check_ssl_support() check_optimize_flag() - set_backend(backend) + + +def init_with_backend(backend): + """Do later stages of init when we know the backend. + + Args: + backend: The backend as usertypes.Backend member. + """ + assert not isinstance(backend, str), backend + assert backend is not None + check_backend_libraries(backend) + check_backend_ssl_support(backend) + check_new_webkit(backend) diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py index 9af210498..60d764620 100644 --- a/qutebrowser/misc/objects.py +++ b/qutebrowser/misc/objects.py @@ -22,5 +22,14 @@ # NOTE: We need to be careful with imports here, as this is imported from # earlyinit. + +class NoBackend: + + """Special object when there's no backend set so we notice that.""" + + def __eq__(self, other): + raise AssertionError("No backend set!") + + # A usertypes.Backend member -backend = None +backend = NoBackend() diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index c4a2ad661..7c42b231b 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -32,6 +32,7 @@ from qutebrowser.utils import (standarddir, objreg, qtutils, log, message, from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.config import config, configfiles from qutebrowser.completion.models import miscmodels +from qutebrowser.mainwindow import mainwindow default = object() # Sentinel value @@ -371,7 +372,6 @@ class SessionManager(QObject): name: The name of the session to load. temp: If given, don't set the current session. """ - from qutebrowser.mainwindow import mainwindow path = self._get_session_path(name, check_exists=True) try: with open(path, encoding='utf-8') as f: diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 24cd49914..1ae4ce192 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -95,9 +95,6 @@ def get_argparser(): action='store_false', dest='color') debug.add_argument('--force-color', help="Force colored logging", action='store_true') - debug.add_argument('--harfbuzz', choices=['old', 'new', 'system', 'auto'], - default='auto', help="HarfBuzz engine version to use. " - "Default: auto.") debug.add_argument('--relaxed-config', action='store_true', help="Silently remove unknown config options.") debug.add_argument('--nowindow', action='store_true', help="Don't show " @@ -170,8 +167,8 @@ def main(): # from json. data = json.loads(args.json_args) args = argparse.Namespace(**data) - earlyinit.earlyinit(args) + earlyinit.early_init(args) # We do this imports late as earlyinit needs to be run first (because of - # the harfbuzz fix and version checking). + # version checking and other early initialization) from qutebrowser import app return app.run(args) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 6cdc61f41..3049d5dfe 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -345,7 +345,7 @@ def qt_message_handler(msg_type, context, msg): try: qt_to_logging[QtCore.QtInfoMsg] = logging.INFO except AttributeError: - # Qt < 5.5 + # While we don't support Qt < 5.5 anymore, logging still needs to work pass # Change levels of some well-known messages to debug so they don't get diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 4df08ba0d..df962a165 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -105,13 +105,9 @@ class ObjectRegistry(collections.UserDict): func = partial_objs[name] try: self[name].destroyed.disconnect(func) - except (RuntimeError, TypeError): + except RuntimeError: # If C++ has deleted the object, the slot is already # disconnected. - # - # With older PyQt-versions (5.2.1) we'll get a "TypeError: - # pyqtSignal must be bound to a QObject" instead: - # https://github.com/qutebrowser/qutebrowser/issues/257 pass del partial_objs[name] @@ -145,7 +141,7 @@ class ObjectRegistry(collections.UserDict): for name, obj in self.data.items(): try: obj_repr = repr(obj) - except (RuntimeError, TypeError): + except RuntimeError: # Underlying object deleted probably obj_repr = '' lines.append("{}: {}".format(name, obj_repr)) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 6a3a5e972..bac4bb2b8 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -28,10 +28,10 @@ Module attributes: import io -import os import operator import contextlib +import pkg_resources from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QSaveFile, QT_VERSION_STR) try: @@ -39,17 +39,6 @@ try: except ImportError: # pragma: no cover qWebKitVersion = None -from qutebrowser.utils import log - -with log.ignore_py_warnings(category=PendingDeprecationWarning, module='imp'): - with log.ignore_py_warnings(category=ImportWarning): - # This imports 'imp' and gives us a PendingDeprecationWarning on - # Debian Jessie. - # - # On Archlinux, we get ImportWarning from - # importlib/_bootstrap_external.py for modules with missing __init__. - import pkg_resources - MAXVALS = { 'int': 2 ** 31 - 1, @@ -102,8 +91,8 @@ def version_check(version, exact=False, strict=False): return result -def is_qtwebkit_ng(): - """Check if the given version is QtWebKit-NG.""" +def is_new_qtwebkit(): + """Check if the given version is a new QtWebKit.""" assert qWebKitVersion is not None return (pkg_resources.parse_version(qWebKitVersion()) > pkg_resources.parse_version('538.1')) @@ -137,15 +126,6 @@ def check_overflow(arg, ctype, fatal=True): return arg -def check_print_compat(): - """Check if printing should work in the given Qt version.""" - # WORKAROUND (remove this when we bump the requirements to 5.3.0) - if os.name == 'nt': - return version_check('5.3') - else: - return True - - def ensure_valid(obj): """Ensure a Qt object with an .isValid() method is valid.""" if not obj.isValid(): diff --git a/qutebrowser/utils/typing.py b/qutebrowser/utils/typing.py deleted file mode 100644 index 358a1a5a3..000000000 --- a/qutebrowser/utils/typing.py +++ /dev/null @@ -1,68 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2016-2017 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -# pylint: disable=unused-import,bad-mcs-method-argument - -"""Wrapper for Python 3.5's typing module. - -This wrapper is needed as both Python 3.5 and typing for PyPI isn't commonly -packaged yet. As we don't actually need anything from the typing module at -runtime, we instead mock the typing classes (using objects to make things -easier) so the typing module isn't a hard dependency. -""" - -# Those are defined here to make them testable easily - - -class FakeTypingMeta(type): - - """Fake typing metaclass like typing.TypingMeta.""" - - def __init__(self, *args, # pylint: disable=super-init-not-called - **_kwds): - pass - - -class FakeUnionMeta(FakeTypingMeta): - - """Fake union metaclass metaclass like typing.UnionMeta.""" - - def __new__(cls, name, bases, namespace, parameters=None): - if parameters is None: - return super().__new__(cls, name, bases, namespace) - self = super().__new__(cls, name, bases, {}) - self.__union_params__ = tuple(parameters) - return self - - def __getitem__(self, parameters): - return self.__class__(self.__name__, self.__bases__, - dict(self.__dict__), parameters=parameters) - - -class FakeUnion(metaclass=FakeUnionMeta): - - """Fake Union type like typing.Union.""" - - __union_params__ = None - - -try: - from typing import Union -except ImportError: # pragma: no cover - Union = FakeUnion diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index d57566e57..1bc928697 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -355,14 +355,8 @@ class Question(QObject): log.misc.debug("Question was already aborted") return self.is_aborted = True - try: - self.aborted.emit() - self.completed.emit() - except TypeError: - # WORKAROUND - # We seem to get "pyqtSignal must be bound to a QObject, not - # 'Question' here, which makes no sense at all..." - log.misc.exception("Error while aborting question") + self.aborted.emit() + self.completed.emit() class Timer(QTimer): diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index e795cfdd8..23c37a042 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -42,7 +42,7 @@ except ImportError: # pragma: no cover from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper import qutebrowser -from qutebrowser.utils import qtutils, log +from qutebrowser.utils import qtutils, log, debug fake_clipboard = None @@ -425,14 +425,13 @@ class KeyInfo: self.text = text def __repr__(self): - # Meh, dependency cycle... - from qutebrowser.utils.debug import qenum_key if self.modifiers is None: modifiers = None else: #modifiers = qflags_key(Qt, self.modifiers) modifiers = hex(int(self.modifiers)) - return get_repr(self, constructor=True, key=qenum_key(Qt, self.key), + return get_repr(self, constructor=True, + key=debug.qenum_key(Qt, self.key), modifiers=modifiers, text=self.text) def __eq__(self, other): @@ -814,10 +813,12 @@ def open_file(filename, cmdline=None): the filename is appended to the cmdline. """ # Import late to avoid circular imports: - # utils -> config -> configdata -> configtypes -> cmdutils -> command -> - # utils - from qutebrowser.misc import guiprocess + # - usertypes -> utils -> guiprocess -> message -> usertypes + # - usertypes -> utils -> config -> configdata -> configtypes -> + # cmdutils -> command -> message -> usertypes from qutebrowser.config import config + from qutebrowser.misc import guiprocess + # the default program to open downloads with - will be empty string # if we want to use the default override = config.val.downloads.open_dispatcher diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 39f501bdb..75ac197fa 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -44,7 +44,7 @@ except ImportError: # pragma: no cover QWebEngineProfile = None import qutebrowser -from qutebrowser.utils import log, utils, standarddir, usertypes, qtutils +from qutebrowser.utils import log, utils, standarddir, usertypes from qutebrowser.misc import objects, earlyinit, sql from qutebrowser.browser import pdfjs @@ -190,7 +190,6 @@ def _module_versions(): ('pygments', ['__version__']), ('yaml', ['__version__']), ('cssutils', ['__version__']), - ('typing', []), ('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebKitWidgets', []), ]) @@ -304,9 +303,7 @@ def _chromium_version(): def _backend(): """Get the backend line with relevant information.""" if objects.backend == usertypes.Backend.QtWebKit: - return '{} (WebKit {})'.format( - 'QtWebKit-NG' if qtutils.is_qtwebkit_ng() else 'legacy QtWebKit', - qWebKitVersion()) + return 'new QtWebKit (WebKit {})'.format(qWebKitVersion()) else: webengine = usertypes.Backend.QtWebEngine assert objects.backend == webengine, objects.backend diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index f53769f8b..0848c404b 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -149,8 +149,6 @@ PERFECT_FILES = [ 'utils/jinja.py'), ('tests/unit/utils/test_error.py', 'utils/error.py'), - ('tests/unit/utils/test_typing.py', - 'utils/typing.py'), ('tests/unit/utils/test_javascript.py', 'utils/javascript.py'), @@ -291,7 +289,7 @@ def main_check_all(): tests. This runs pytest with the used executable, so check_coverage.py should be - called with something like ./.tox/py34/bin/python. + called with something like ./.tox/py36/bin/python. """ for test_file, src_file in PERFECT_FILES: if test_file is None: diff --git a/scripts/dev/ci/travis_backtrace.sh b/scripts/dev/ci/travis_backtrace.sh index 764c36116..c94d1ff06 100644 --- a/scripts/dev/ci/travis_backtrace.sh +++ b/scripts/dev/ci/travis_backtrace.sh @@ -5,10 +5,6 @@ # case $TESTENV in - py34-cov) - exe=/usr/bin/python3.4 - full=full - ;; py3*-pyqt*) exe=$(readlink -f .tox/$TESTENV/bin/python) full= diff --git a/scripts/setupcommon.py b/scripts/setupcommon.py index 494ab4c46..f615ad155 100644 --- a/scripts/setupcommon.py +++ b/scripts/setupcommon.py @@ -121,7 +121,8 @@ setupdata = { 'Operating System :: Microsoft :: Windows :: Windows 7', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Browsers', diff --git a/tests/conftest.py b/tests/conftest.py index 1c26e44d7..302d08864 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,7 +62,6 @@ def _apply_platform_markers(config, item): ('no_ci', 'CI' in os.environ, "Skipped on CI."), ('issue2478', os.name == 'nt' and config.webengine, "Broken with QtWebEngine on Windows"), - ('qt55', not qtutils.version_check('5.5'), "Requires Qt 5.5 or newer"), ] for searched_marker, condition, default_reason in markers: @@ -128,12 +127,9 @@ def pytest_collection_modifyitems(config, items): item.add_marker(pytest.mark.xfail(run=False)) if item.get_marker('js_prompt'): if config.webengine: - js_prompt_pyqt_version = 0x050700 - else: - js_prompt_pyqt_version = 0x050300 - item.add_marker(pytest.mark.skipif( - PYQT_VERSION <= js_prompt_pyqt_version, - reason='JS prompts are not supported with this PyQt version')) + item.add_marker(pytest.mark.skipif( + PYQT_VERSION <= 0x050700, + reason='JS prompts are not supported with PyQt 5.7')) if deselected: deselected_items.append(item) diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index 75c6845f4..e9d2a8e95 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -109,8 +109,6 @@ def _get_backend_tag(tag): 'qtwebengine_todo': pytest.mark.qtwebengine_todo, 'qtwebengine_skip': pytest.mark.qtwebengine_skip, 'qtwebkit_skip': pytest.mark.qtwebkit_skip, - 'qtwebkit_ng_xfail': pytest.mark.qtwebkit_ng_xfail, - 'qtwebkit_ng_skip': pytest.mark.qtwebkit_ng_skip, } if not any(tag.startswith(t + ':') for t in pytest_marks): return None @@ -143,10 +141,6 @@ def pytest_collection_modifyitems(config, items): config.webengine), ('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif, not config.webengine), - ('qtwebkit_ng_xfail', 'Failing with QtWebKit-NG', pytest.mark.xfail, - not config.webengine and qtutils.is_qtwebkit_ng()), - ('qtwebkit_ng_skip', 'Skipped with QtWebKit-NG', pytest.mark.skipif, - not config.webengine and qtutils.is_qtwebkit_ng()), ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif, config.webengine), ('qtwebengine_mac_xfail', 'Fails on macOS with QtWebEngine', diff --git a/tests/end2end/data/javascript/window_open.html b/tests/end2end/data/javascript/window_open.html index b8e178db3..f842380d6 100644 --- a/tests/end2end/data/javascript/window_open.html +++ b/tests/end2end/data/javascript/window_open.html @@ -6,11 +6,7 @@ var my_window; function open_modal() { - if (window.showModalDialog) { - window.showModalDialog(); - } else { - window.open('about:blank', 'window', 'modal'); - } + window.open('about:blank', 'window', 'modal'); } function open_normal() { diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature index a340db429..be4035f37 100644 --- a/tests/end2end/features/history.feature +++ b/tests/end2end/features/history.feature @@ -103,13 +103,4 @@ Feature: Page history And I open qute:history without waiting And I wait until qute://history is loaded Then the page should contain the plaintext "3.txt" - Then the page should contain the plaintext "4.txt" - - ## Bugs - - @qtwebengine_skip @qtwebkit_ng_skip - Scenario: Opening a valid URL which turns out invalid - When I set url.auto_search to naive - And I run :open http://foo%40bar@baz - Then "QFSFileEngine::open: No file name specified" should be logged - And "Error while loading : Host not found" should be logged + Then the page should contain the plaintext "4.txt" \ No newline at end of file diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index 8e7be7fec..c685722fa 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -17,7 +17,7 @@ Feature: Javascript stuff And I run :click-element id close-normal Then "Focus object changed: *" should be logged - @qtwebkit_ng_skip + @qtwebkit_skip Scenario: Opening/closing a modal window via JS When I open data/javascript/window_open.html And I run :tab-only @@ -26,7 +26,6 @@ Feature: Javascript stuff And I run :tab-focus 1 And I run :click-element id close-normal Then "Focus object changed: *" should be logged - # WebModalDialog with QtWebKit, WebDialog with QtWebEngine And "Web*Dialog requested, but we don't support that!" should be logged # https://github.com/qutebrowser/qutebrowser/issues/906 diff --git a/tests/end2end/features/private.feature b/tests/end2end/features/private.feature index 28bf37b9e..35097f545 100644 --- a/tests/end2end/features/private.feature +++ b/tests/end2end/features/private.feature @@ -42,7 +42,6 @@ Feature: Using private browsing ## https://github.com/qutebrowser/qutebrowser/issues/1219 - @qtwebkit_ng_skip: private browsing is not implemented yet Scenario: Sharing cookies with private browsing When I open cookies/set?qute-test=42 without waiting in a private window And I wait until cookies is loaded diff --git a/tests/end2end/features/test_yankpaste_bdd.py b/tests/end2end/features/test_yankpaste_bdd.py index deabba59f..c891bf7b4 100644 --- a/tests/end2end/features/test_yankpaste_bdd.py +++ b/tests/end2end/features/test_yankpaste_bdd.py @@ -21,8 +21,6 @@ import pytest import pytest_bdd as bdd -from PyQt5.QtCore import PYQT_VERSION - bdd.scenarios('yankpaste.feature') @@ -34,10 +32,6 @@ def init_fake_clipboard(quteproc): @bdd.when(bdd.parsers.parse('I insert "{value}" into the text field')) -def set_text_field(request, quteproc, value): - if request.config.webengine and PYQT_VERSION >= 0x50700: - cmd = ":jseval --world=0 set_text('{}')".format(value) - else: - cmd = ":jseval set_text('{}')".format(value) - quteproc.send_cmd(cmd) +def set_text_field(quteproc, value): + quteproc.send_cmd(":jseval --world=0 set_text('{}')".format(value)) quteproc.wait_for_js('textarea set to: ' + value) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index a61372450..a59ea5db7 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -29,8 +29,6 @@ import pytest from PyQt5.QtCore import QProcess -from qutebrowser.utils import qtutils - def _base_args(config): """Get the arguments to pass with every invocation.""" @@ -188,8 +186,6 @@ def test_version(request): assert re.search(r'^qutebrowser\s+v\d+(\.\d+)', stdout) is not None -@pytest.mark.skipif(not qtutils.version_check('5.3'), - reason="Does not work on Qt 5.2") def test_qt_arg(request, quteproc_new, tmpdir): """Test --qt-arg.""" args = (['--temp-basedir', '--qt-arg', 'stylesheet', @@ -269,7 +265,7 @@ def test_launching_with_python2(): pytest.skip("python2 not found") _stdout, stderr = proc.communicate() assert proc.returncode == 1 - error = "At least Python 3.4 is required to run qutebrowser" + error = "At least Python 3.5 is required to run qutebrowser" assert stderr.decode('ascii').startswith(error) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 6cd038ce0..08aea97f6 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -43,7 +43,7 @@ from qutebrowser.browser.webkit import cookies from qutebrowser.misc import savemanager, sql from qutebrowser.keyinput import modeman -from PyQt5.QtCore import PYQT_VERSION, pyqtSignal, QEvent, QSize, Qt, QObject +from PyQt5.QtCore import pyqtSignal, QEvent, QSize, Qt, QObject from PyQt5.QtGui import QKeyEvent from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout from PyQt5.QtNetwork import QNetworkCookieJar @@ -156,8 +156,6 @@ def tab_registry(win_registry): @pytest.fixture def fake_web_tab(stubs, tab_registry, mode_manager, qapp): """Fixture providing the FakeWebTab *class*.""" - if PYQT_VERSION < 0x050600: - pytest.skip('Causes segfaults, see #1638') return stubs.FakeWebTab @@ -417,8 +415,9 @@ def fake_save_manager(): @pytest.fixture -def fake_args(): +def fake_args(request): ns = types.SimpleNamespace() + ns.backend = 'webengine' if request.config.webengine else 'webkit' objreg.register('args', ns) yield ns objreg.delete('args') diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index e6b081f18..a2a34d9ce 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -19,8 +19,6 @@ import pytest -from PyQt5.QtCore import PYQT_VERSION - from qutebrowser.browser import browsertab pytestmark = pytest.mark.usefixtures('redirect_webengine_data') @@ -48,9 +46,6 @@ def view(qtbot, config_stub, request): @pytest.fixture(params=['webkit', 'webengine']) def tab(request, qtbot, tab_registry, cookiejar_and_cache, mode_manager): - if PYQT_VERSION < 0x050600: - pytest.skip('Causes segfaults, see #1638') - if request.param == 'webkit': webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab') tab_class = webkittab.WebKitTab diff --git a/tests/unit/browser/webkit/network/test_pac.py b/tests/unit/browser/webkit/network/test_pac.py index 1520ff13c..ea8a7acaf 100644 --- a/tests/unit/browser/webkit/network/test_pac.py +++ b/tests/unit/browser/webkit/network/test_pac.py @@ -20,10 +20,9 @@ import http.server import threading import logging -import sys import pytest -from PyQt5.QtCore import QUrl, QT_VERSION_STR +from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import (QNetworkProxy, QNetworkProxyQuery, QHostInfo, QHostAddress) @@ -206,14 +205,6 @@ def test_secret_url(url, has_secret, from_file): res.resolve(QNetworkProxyQuery(QUrl(url)), from_file=from_file) -# See https://github.com/qutebrowser/qutebrowser/pull/1891#issuecomment-259222615 - -try: - from PyQt5 import QtWebEngineWidgets -except ImportError: - QtWebEngineWidgets = None - - def fetcher_test(test_str): class PACHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): @@ -244,10 +235,6 @@ def fetcher_test(test_str): return fetcher -@pytest.mark.skipif(QT_VERSION_STR.startswith('5.7') and - QtWebEngineWidgets is not None and - sys.platform == "linux", - reason="Segfaults when run with QtWebEngine tests on Linux") def test_fetch_success(): test_str = """ function FindProxyForURL(domain, host) { @@ -260,10 +247,6 @@ def test_fetch_success(): assert len(proxies) == 3 -@pytest.mark.skipif(QT_VERSION_STR.startswith('5.7') and - QtWebEngineWidgets is not None and - sys.platform == "linux", - reason="Segfaults when run with QtWebEngine tests on Linux") def test_fetch_evalerror(caplog): test_str = """ function FindProxyForURL(domain, host) { diff --git a/tests/unit/commands/test_cmdutils.py b/tests/unit/commands/test_cmdutils.py index 6d610d7b0..2da13881c 100644 --- a/tests/unit/commands/test_cmdutils.py +++ b/tests/unit/commands/test_cmdutils.py @@ -24,11 +24,12 @@ import sys import logging import types +import typing import pytest from qutebrowser.commands import cmdutils, cmdexc, argparser, command -from qutebrowser.utils import usertypes, typing +from qutebrowser.utils import usertypes @pytest.fixture(autouse=True) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 3a151a939..459745438 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -877,6 +877,9 @@ def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir, monkeypatch.setattr(config, 'key_instance', None) monkeypatch.setattr(config, '_change_filters', []) monkeypatch.setattr(config, '_init_errors', []) + # Make sure we get no SSL warning + monkeypatch.setattr(config.earlyinit, 'check_backend_ssl_support', + lambda _backend: None) yield try: objreg.delete('config-commands') @@ -888,7 +891,7 @@ def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir, @pytest.mark.parametrize('config_py', [True, 'error', False]) @pytest.mark.parametrize('invalid_yaml', ['42', 'unknown', False]) # pylint: disable=too-many-branches -def test_early_init(init_patch, config_tmpdir, caplog, +def test_early_init(init_patch, config_tmpdir, caplog, fake_args, load_autoconfig, config_py, invalid_yaml): # Prepare files autoconfig_file = config_tmpdir / 'autoconfig.yml' @@ -914,7 +917,7 @@ def test_early_init(init_patch, config_tmpdir, caplog, 'utf-8', ensure=True) with caplog.at_level(logging.ERROR): - config.early_init() + config.early_init(fake_args) # Check error messages expected_errors = [] @@ -954,15 +957,16 @@ def test_early_init(init_patch, config_tmpdir, caplog, assert config.instance._values == {'colors.hints.fg': 'magenta'} -def test_early_init_invalid_change_filter(init_patch): +def test_early_init_invalid_change_filter(init_patch, fake_args): config.change_filter('foobar') with pytest.raises(configexc.NoOptionError): - config.early_init() + config.early_init(fake_args) @pytest.mark.parametrize('errors', [True, False]) -def test_late_init(init_patch, monkeypatch, fake_save_manager, mocker, errors): - config.early_init() +def test_late_init(init_patch, monkeypatch, fake_save_manager, fake_args, + mocker, errors): + config.early_init(fake_args) if errors: err = configexc.ConfigErrorDesc("Error text", Exception("Exception")) errs = configexc.ConfigFileErrors("config.py", [err]) diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index aeab032e7..a25e1d187 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -28,7 +28,7 @@ import pytest from qutebrowser.misc import checkpyver -TEXT = (r"At least Python 3.4 is required to run qutebrowser, but " +TEXT = (r"At least Python 3.5 is required to run qutebrowser, but " r"\d+\.\d+\.\d+ is installed!\n") @@ -60,7 +60,7 @@ def test_patched_no_errwindow(capfd, monkeypatch): """Test with a patched sys.hexversion and --no-err-windows.""" monkeypatch.setattr(checkpyver.sys, 'argv', [sys.argv[0], '--no-err-windows']) - monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03000000) + monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03040000) monkeypatch.setattr(checkpyver.sys, 'exit', lambda status: None) checkpyver.check_python_version() stdout, stderr = capfd.readouterr() @@ -70,7 +70,7 @@ def test_patched_no_errwindow(capfd, monkeypatch): def test_patched_errwindow(capfd, mocker, monkeypatch): """Test with a patched sys.hexversion and a fake Tk.""" - monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03000000) + monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03040000) monkeypatch.setattr(checkpyver.sys, 'exit', lambda status: None) try: diff --git a/tests/unit/misc/test_earlyinit.py b/tests/unit/misc/test_earlyinit.py index cf67690fd..fa63dc5b0 100644 --- a/tests/unit/misc/test_earlyinit.py +++ b/tests/unit/misc/test_earlyinit.py @@ -19,11 +19,7 @@ """Test qutebrowser.misc.earlyinit.""" -import os import sys -import types -import logging -import pkg_resources import pytest @@ -35,94 +31,3 @@ def test_init_faulthandler_stderr_none(monkeypatch, attr): """Make sure init_faulthandler works when sys.stderr/__stderr__ is None.""" monkeypatch.setattr(sys, attr, None) earlyinit.init_faulthandler() - - -class TestFixHarfbuzz: - - @pytest.fixture(autouse=True) - def clear_harfbuzz(self): - """Clear QT_HARFBUZZ before/after tests.""" - old_harfbuzz = os.environ.pop('QT_HARFBUZZ', None) - yield - if old_harfbuzz is None: - os.environ.pop('QT_HARFBUZZ', None) - else: - os.environ['QT_HARFBUZZ'] = old_harfbuzz - - @pytest.fixture - def args(self): - """Get a fake argparse namespace.""" - return types.SimpleNamespace(harfbuzz='auto') - - @pytest.mark.parametrize('harfbuzz, qt_version, platform, expected', [ - ('auto', '5.2.1', 'linux', 'old'), - ('auto', '5.3.0', 'linux', 'new'), - ('auto', '5.3.2', 'linux', 'old'), - ('auto', '5.4.0', 'linux', None), - - ('auto', '5.2.1', 'windows', None), - - ('old', '5.3.0', 'linux', 'old'), - ('old', '5.4.0', 'linux', 'old'), - - ('new', '5.2.1', 'linux', 'new'), - ('new', '5.3.2', 'linux', 'new'), - ('new', '5.4.0', 'linux', 'new'), - ]) - def test_fix_harfbuzz(self, clear_harfbuzz, args, monkeypatch, caplog, - harfbuzz, qt_version, platform, expected): - """Check the QT_HARFBUZZ env var.""" - args.harfbuzz = harfbuzz - monkeypatch.setattr(earlyinit, '_qt_version', - lambda: pkg_resources.parse_version(qt_version)) - monkeypatch.setattr(earlyinit.sys, 'platform', platform) - - with caplog.at_level(logging.WARNING): - # Because QtWidgets is already imported - earlyinit.fix_harfbuzz(args) - - assert os.environ.get('QT_HARFBUZZ', None) == expected - - @pytest.mark.parametrize('frozen, level', [ - (True, logging.DEBUG), - (False, logging.WARNING), - ]) - def test_widgets_warning(self, args, monkeypatch, caplog, frozen, level): - """Make sure fix_harfbuzz warns when QtWidgets is imported.""" - # Make sure QtWidgets is in sys.modules - from PyQt5 import QtWidgets # pylint: disable=unused-variable - if frozen: - monkeypatch.setattr(earlyinit.sys, 'frozen', True, raising=False) - else: - monkeypatch.delattr(earlyinit.sys, 'frozen', raising=False) - - with caplog.at_level(level): - earlyinit.fix_harfbuzz(args) - - record = caplog.records[0] - assert record.levelno == level - msg = "Harfbuzz fix attempted but QtWidgets is already imported!" - assert record.message == msg - - def test_no_warning(self, args, monkeypatch): - """Without QtWidgets in sys.modules, no warning should be shown.""" - monkeypatch.setattr(earlyinit.sys, 'modules', {}) - earlyinit.fix_harfbuzz(args) - - -@pytest.mark.parametrize('same', [True, False]) -def test_qt_version(same): - if same: - qt_version_str = '5.4.0' - expected = '5.4.0' - else: - qt_version_str = '5.3.0' - expected = '5.4.0 (compiled 5.3.0)' - actual = earlyinit.qt_version(qversion='5.4.0', - qt_version_str=qt_version_str) - assert actual == expected - - -def test_qt_version_no_args(): - """Make sure qt_version without arguments at least works.""" - earlyinit.qt_version() diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index c9e1e3913..a6a663a0d 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -35,7 +35,7 @@ from PyQt5.QtTest import QSignalSpy import qutebrowser from qutebrowser.misc import ipc -from qutebrowser.utils import objreg, qtutils, standarddir +from qutebrowser.utils import objreg, standarddir from helpers import stubs @@ -778,26 +778,7 @@ def test_connect_inexistent(qlocalsocket): assert qlocalsocket.error() == QLocalSocket.ServerNotFoundError -def test_socket_options_listen_problem(qlocalserver, short_tmpdir): - """In earlier versions of Qt, listening fails when using socketOptions. - - With this test, we verify that this bug exists in the Qt version/OS - combinations we expect it to, and doesn't exist in other versions. - """ - servername = str(short_tmpdir / 'x') - qlocalserver.setSocketOptions(QLocalServer.UserAccessOption) - ok = qlocalserver.listen(servername) - if os.name == 'nt' or qtutils.version_check('5.4'): - assert ok - else: - assert not ok - assert qlocalserver.serverError() == QAbstractSocket.HostNotFoundError - assert qlocalserver.errorString() == 'QLocalServer::listen: Name error' - - @pytest.mark.posix -@pytest.mark.skipif(not qtutils.version_check('5.4'), - reason="setSocketOptions is even more broken on Qt < 5.4.") def test_socket_options_address_in_use_problem(qlocalserver, short_tmpdir): """Qt seems to ignore AddressInUseError when using socketOptions. diff --git a/tests/unit/misc/test_objects.py b/tests/unit/misc/test_objects.py new file mode 100644 index 000000000..7de92b12d --- /dev/null +++ b/tests/unit/misc/test_objects.py @@ -0,0 +1,33 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +import pytest + +from qutebrowser.misc import objects +from qutebrowser.utils import usertypes + + +@pytest.mark.parametrize('func', [ + lambda: objects.NoBackend() == usertypes.Backend.QtWebEngine, + lambda: objects.NoBackend() != usertypes.Backend.QtWebEngine, + lambda: objects.NoBackend() in [usertypes.Backend.QtWebEngine], +]) +def test_no_backend(func): + with pytest.raises(AssertionError, match='No backend set!'): + func() diff --git a/tests/unit/utils/test_javascript.py b/tests/unit/utils/test_javascript.py index 54c32ee92..82974876f 100644 --- a/tests/unit/utils/test_javascript.py +++ b/tests/unit/utils/test_javascript.py @@ -19,13 +19,9 @@ """Tests for qutebrowser.utils.javascript.""" -import binascii -import os.path - import pytest import hypothesis import hypothesis.strategies -from PyQt5.QtCore import PYQT_VERSION from qutebrowser.utils import javascript @@ -61,65 +57,22 @@ class TestStringEscape: """Test javascript escaping with some expected outcomes.""" assert javascript.string_escape(before) == after - def _test_escape(self, text, qtbot, webframe): - """Helper function for test_real_escape*.""" - try: - self._test_escape_simple(text, webframe) - except AssertionError: - # Try another method if the simple method failed. - # - # See _test_escape_hexlified documentation on why this is - # necessary. - self._test_escape_hexlified(text, qtbot, webframe) - - def _test_escape_hexlified(self, text, qtbot, webframe): - """Test conversion by hexlifying in javascript. - - Since the conversion of QStrings to Python strings is broken in some - older PyQt versions in some corner cases, we load an HTML file which - generates an MD5 of the escaped text and use that for comparisons. - """ - escaped = javascript.string_escape(text) - path = os.path.join(os.path.dirname(__file__), - 'test_javascript_string_escape.html') - with open(path, encoding='utf-8') as f: - html_source = f.read().replace('%INPUT%', escaped) - - with qtbot.waitSignal(webframe.loadFinished) as blocker: - webframe.setHtml(html_source) - assert blocker.args == [True] - - result = webframe.evaluateJavaScript('window.qute_test_result') - assert result is not None - assert '|' in result - result_md5, result_text = result.split('|', maxsplit=1) - text_md5 = binascii.hexlify(text.encode('utf-8')).decode('ascii') - assert result_md5 == text_md5, result_text - - def _test_escape_simple(self, text, webframe): + def _test_escape(self, text, webframe): """Test conversion by using evaluateJavaScript.""" escaped = javascript.string_escape(text) result = webframe.evaluateJavaScript('"{}";'.format(escaped)) assert result == text @pytest.mark.parametrize('text', sorted(TESTS), ids=repr) - def test_real_escape(self, webframe, qtbot, text): + def test_real_escape(self, webframe, text): """Test javascript escaping with a real QWebPage.""" - self._test_escape(text, qtbot, webframe) + self._test_escape(text, webframe) @pytest.mark.qt_log_ignore('^OpenType support missing for script') @hypothesis.given(hypothesis.strategies.text()) - def test_real_escape_hypothesis(self, webframe, qtbot, text): + def test_real_escape_hypothesis(self, webframe, text): """Test javascript escaping with a real QWebPage and hypothesis.""" - # We can't simply use self._test_escape because of this: - # https://github.com/pytest-dev/pytest-qt/issues/69 - - # self._test_escape(text, qtbot, webframe) - try: - self._test_escape_simple(text, webframe) - except AssertionError: - if PYQT_VERSION >= 0x050300: - self._test_escape_hexlified(text, qtbot, webframe) + self._test_escape(text, webframe) @pytest.mark.parametrize('arg, expected', [ diff --git a/tests/unit/utils/test_javascript_string_escape.html b/tests/unit/utils/test_javascript_string_escape.html deleted file mode 100644 index 31bda35ef..000000000 --- a/tests/unit/utils/test_javascript_string_escape.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - -

set_text() not called...

- diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index f0fda7a2e..f3c1afc04 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -82,14 +82,14 @@ def test_version_check(monkeypatch, qversion, compiled, version, exact, assert qtutils.version_check(version, exact, strict=strict) == expected -@pytest.mark.parametrize('version, ng', [ +@pytest.mark.parametrize('version, is_new', [ ('537.21', False), # QtWebKit 5.1 ('538.1', False), # Qt 5.8 - ('602.1', True) # QtWebKit-NG TP5 + ('602.1', True) # new QtWebKit TP5, 5.212 Alpha ]) -def test_is_qtwebkit_ng(monkeypatch, version, ng): +def test_is_new_qtwebkit(monkeypatch, version, is_new): monkeypatch.setattr(qtutils, 'qWebKitVersion', lambda: version) - assert qtutils.is_qtwebkit_ng() == ng + assert qtutils.is_new_qtwebkit() == is_new class TestCheckOverflow: @@ -118,26 +118,6 @@ class TestCheckOverflow: assert newval == repl -@pytest.mark.parametrize('os_name, qversion, expected', [ - ('linux', '5.2.1', True), # unaffected OS - ('linux', '5.4.1', True), # unaffected OS - ('nt', '5.2.1', False), - ('nt', '5.3.0', True), # unaffected Qt version - ('nt', '5.4.1', True), # unaffected Qt version -]) -def test_check_print_compat(os_name, qversion, expected, monkeypatch): - """Test check_print_compat. - - Args: - os_name: The fake os.name to set. - qversion: The fake qVersion() to set. - expected: The expected return value. - """ - monkeypatch.setattr(qtutils.os, 'name', os_name) - monkeypatch.setattr(qtutils, 'qVersion', lambda: qversion) - assert qtutils.check_print_compat() == expected - - class QtObject: """Fake Qt object for test_ensure.""" diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index b828926e9..d652310d5 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -87,8 +87,6 @@ def test_fake_mac_config(tmpdir, monkeypatch): assert standarddir.config() == expected -# FIXME:conf needs AppDataLocation -@pytest.mark.qt55 @pytest.mark.parametrize('what', ['data', 'config', 'cache']) @pytest.mark.not_mac def test_fake_windows(tmpdir, monkeypatch, what): @@ -354,8 +352,6 @@ class TestSystemData: assert standarddir.data(system=True) == standarddir.data() -# FIXME:conf needs AppDataLocation -@pytest.mark.qt55 class TestMoveWindowsAndMacOS: """Test other invocations of _move_data.""" diff --git a/tests/unit/utils/test_typing.py b/tests/unit/utils/test_typing.py deleted file mode 100644 index 4a3359a45..000000000 --- a/tests/unit/utils/test_typing.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2016-2017 Florian Bruhin (The Compiler) -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Tests for qutebrowser.utils.typing.""" - -import pytest - -from qutebrowser.utils import typing - - -@pytest.fixture -def pytyping(): - """A fixture to get the python 3.5+ typing module.""" - pytyping = pytest.importorskip('typing') - return pytyping - - -class TestUnion: - - def test_python_subclass(self, pytyping): - assert (type(pytyping.Union[str, int]) is # flake8: disable=E721 - type(pytyping.Union)) - - def test_qute_subclass(self): - assert (type(typing.FakeUnion[str, int]) is # flake8: disable=E721 - type(typing.FakeUnion)) - - def test_python_params(self, pytyping): - union = pytyping.Union[str, int] - try: - assert union.__union_params__ == (str, int) - except AttributeError: - assert union.__args__ == (str, int) - - def test_qute_params(self): - union = typing.FakeUnion[str, int] - try: - assert union.__union_params__ == (str, int) - except AttributeError: - assert union.__args__ == (str, int) diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index a6ae9104b..a6b1242d1 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -328,9 +328,8 @@ def test_get_search_url_invalid(url): (False, True, True, 'deadbeef'), (False, True, True, 'hello.'), (False, True, False, 'site:cookies.com oatmeal raisin'), - # no DNS because bogus-IP - (False, True, False, '31c3'), - (False, True, False, 'foo::bar'), # no DNS because of no host + # no DNS because there is no host + (False, True, False, 'foo::bar'), # Valid search term with autosearch (False, False, False, 'test foo'), # autosearch = False @@ -350,11 +349,6 @@ def test_is_url(config_stub, fake_dns, url: The URL to test, as a string. auto_search: With which auto_search setting to test """ - if (url == '31c3' and - auto_search == 'dns' and - qtutils.version_check('5.6.1')): - pytest.xfail("Qt behavior changed") - config_stub.val.url.auto_search = auto_search if auto_search == 'dns': if uses_dns: diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 8736d875e..4d977540d 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -35,7 +35,7 @@ import pkg_resources import pytest import qutebrowser -from qutebrowser.utils import version, usertypes, qtutils +from qutebrowser.utils import version, usertypes from qutebrowser.browser import pdfjs @@ -515,12 +515,10 @@ class ImportFake: ('pygments', True), ('yaml', True), ('cssutils', True), - ('typing', True), ('PyQt5.QtWebEngineWidgets', True), ('PyQt5.QtWebKitWidgets', True), ]) - self.no_version_attribute = ['sip', 'typing', - 'PyQt5.QtWebEngineWidgets', + self.no_version_attribute = ['sip', 'PyQt5.QtWebEngineWidgets', 'PyQt5.QtWebKitWidgets'] self.version_attribute = '__version__' self.version = '1.2.3' @@ -588,7 +586,6 @@ class TestModuleVersions: @pytest.mark.parametrize('module, idx, expected', [ ('colorama', 1, 'colorama: no'), ('cssutils', 6, 'cssutils: no'), - ('typing', 7, 'typing: no'), ]) def test_missing_module(self, module, idx, expected, import_fake): """Test with a module missing. @@ -839,7 +836,6 @@ class VersionParams: VersionParams('frozen', frozen=True), VersionParams('no-style', style=False), VersionParams('no-webkit', with_webkit=False), - VersionParams('webkit-ng', with_webkit='ng'), VersionParams('unknown-dist', known_distribution=False), VersionParams('no-ssl', ssl_support=False), ], ids=lambda param: param.name) @@ -884,13 +880,7 @@ def test_version_output(params, stubs, monkeypatch): patches['qWebKitVersion'] = lambda: 'WEBKIT VERSION' patches['objects.backend'] = usertypes.Backend.QtWebKit patches['QWebEngineProfile'] = None - if params.with_webkit == 'ng': - backend = 'QtWebKit-NG' - patches['qtutils.is_qtwebkit_ng'] = lambda: True - else: - backend = 'legacy QtWebKit' - patches['qtutils.is_qtwebkit_ng'] = lambda: False - substitutions['backend'] = backend + ' (WebKit WEBKIT VERSION)' + substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)' else: monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False) patches['objects.backend'] = usertypes.Backend.QtWebEngine @@ -946,8 +936,6 @@ def test_version_output(params, stubs, monkeypatch): assert version.version() == expected -@pytest.mark.skipif(not qtutils.version_check('5.4'), - reason="Needs Qt >= 5.4.") def test_opengl_vendor(): """Simply call version.opengl_vendor() and see if it doesn't crash.""" pytest.importorskip("PyQt5.QtOpenGL") diff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py index b11c01b6a..ebb209326 100644 --- a/tests/unit/utils/usertypes/test_question.py +++ b/tests/unit/utils/usertypes/test_question.py @@ -19,8 +19,6 @@ """Tests for usertypes.Question.""" -import logging - import pytest from qutebrowser.utils import usertypes @@ -82,15 +80,6 @@ def test_abort(question, qtbot): assert question.is_aborted -def test_abort_typeerror(question, qtbot, mocker, caplog): - """Test Question.abort() with .emit() raising a TypeError.""" - signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted') - signal_mock.emit.side_effect = TypeError - with caplog.at_level(logging.ERROR, 'misc'): - question.abort() - assert caplog.records[0].message == 'Error while aborting question' - - def test_abort_twice(question, qtbot): """Abort a question twice.""" with qtbot.wait_signal(question.aborted):