diff options
author | Florian Bruhin <me@the-compiler.org> | 2023-07-22 12:36:31 +0200 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2023-07-22 12:36:31 +0200 |
commit | 273230eb07eb848e67abc8c1e6fc95cfe4c46b6f (patch) | |
tree | a198f4b32f439f3602c25a4942b9a0aabe511b87 | |
parent | 1387b0598b90501dfa1dc8e4cbe5e1d0d05cd048 (diff) | |
parent | ed70741587d0d7f5cc90b2f510c38474a7ad9375 (diff) | |
download | qutebrowser-273230eb07eb848e67abc8c1e6fc95cfe4c46b6f.tar.gz qutebrowser-273230eb07eb848e67abc8c1e6fc95cfe4c46b6f.zip |
Merge remote-tracking branch 'origin/pr/7789'
42 files changed, 441 insertions, 313 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64dddd2f8..580e532f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,8 @@ jobs: - name: Install dependencies run: | [[ ${{ matrix.testenv }} == eslint ]] && npm install -g eslint - [[ ${{ matrix.testenv }} == docs ]] && sudo apt-get update && sudo apt-get install --no-install-recommends asciidoc + [[ ${{ matrix.testenv }} == docs ]] && sudo apt-get update && sudo apt-get install --no-install-recommends asciidoc libegl1-mesa + [[ ${{ matrix.testenv }} == vulture || ${{ matrix.testenv }} == pylint ]] && sudo apt-get update && sudo apt-get install --no-install-recommends libegl1-mesa if [[ ${{ matrix.testenv }} == shellcheck ]]; then scversion="stable" bindir="$HOME/.local/bin" @@ -89,17 +90,16 @@ jobs: fail-fast: false matrix: include: - - testenv: py + - testenv: py-qt5 image: archlinux-webkit - - testenv: py + - testenv: py-qt5 image: archlinux-webengine - - testenv: py-qt6 + - testenv: py-qt5 + image: archlinux-webengine-unstable + - testenv: py image: archlinux-webengine-qt6 - testenv: py - image: archlinux-webengine-unstable - args: "" - # - testenv: py - # image: archlinux-webengine-unstable-qt6 # FIXME:qt6.5 activate + image: archlinux-webengine-unstable-qt6 container: image: "qutebrowser/ci:${{ matrix.image }}" env: @@ -115,9 +115,9 @@ jobs: with: persist-credentials: false - name: Set up problem matchers - run: "python scripts/dev/ci/problemmatchers.py py38 ${{ runner.temp }}" + run: "python scripts/dev/ci/problemmatchers.py tests ${{ runner.temp }}" - name: Run tox - run: "dbus-run-session -- tox -e ${{ matrix.testenv }} -- ${{ matrix.args }}" + run: "dbus-run-session -- tox -e ${{ matrix.testenv }}" tests: if: "!contains(github.event.head_commit.message, '[ci skip]')" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cabf2d8c4..68d2243a4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,6 +15,7 @@ jobs: - archlinux-webkit - archlinux-webengine - archlinux-webengine-unstable + - archlinux-webengine-unstable-qt6 - archlinux-webengine-qt6 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2254abb4a..c1a8dda8a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,50 +16,50 @@ jobs: include: - os: macos-11 branch: master - toxenv: build-release - name: macos + toxenv: build-release-qt5 + name: qt5-macos - os: windows-2019 args: --64bit branch: master - toxenv: build-release - name: windows-64bit + toxenv: build-release-qt5 + name: qt5-windows-64bit - os: windows-2019 args: --32bit branch: master - toxenv: build-release - name: windows-32bit + toxenv: build-release-qt5 + name: qt5-windows-32bit - os: macos-11 args: --debug branch: master - toxenv: build-release - name: macos-debug + toxenv: build-release-qt5 + name: qt5-macos-debug - os: windows-2019 args: --64bit --debug branch: master - toxenv: build-release - name: windows-64bit-debug + toxenv: build-release-qt5 + name: qt5-windows-64bit-debug - os: windows-2019 args: --32bit --debug branch: master - toxenv: build-release - name: windows-32bit-debug + toxenv: build-release-qt5 + name: qt5-windows-32bit-debug - os: macos-11 - toxenv: build-release-qt6 - name: qt6-macos + toxenv: build-release + name: macos - os: windows-2019 args: --64bit - toxenv: build-release-qt6 - name: qt6-windows-64bit + toxenv: build-release + name: windows-64bit - os: macos-11 args: --debug - toxenv: build-release-qt6 - name: qt6-macos-debug + toxenv: build-release + name: macos-debug - os: windows-2019 args: --64bit --debug - toxenv: build-release-qt6 - name: qt6-windows-64bit-debug + toxenv: build-release + name: windows-64bit-debug runs-on: "${{ matrix.os }}" timeout-minutes: 45 steps: @@ -1,6 +1,6 @@ [MASTER] ignore=resources.py -extension-pkg-whitelist=PyQt5,sip +extension-pkg-whitelist=PyQt5,PyQt6,sip load-plugins=qute_pylint.config, pylint.extensions.docstyle, pylint.extensions.emptystring, @@ -58,8 +58,8 @@ disable=locally-disabled, missing-type-doc, missing-param-doc, useless-param-doc, - wrong-import-order, # FIXME:qt6 (lint) - ungrouped-imports, # FIXME:qt6 (lint) + wrong-import-order, # doesn't work with qutebrowser.qt, even with known-third-party set + ungrouped-imports, # ditto [BASIC] function-rgx=[a-z_][a-z0-9_]{2,50}$ diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 666e24177..b8bce1545 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -19,6 +19,17 @@ breaking changes (such as renamed commands) can happen in minor releases. v3.0.0 (unreleased) ------------------- +Major changes +~~~~~~~~~~~~~ + +- qutebrowser now supports Qt 6 and uses it by default. Qt 5.15 is used as a + fallback if Qt 6 is unavailable. This behavior can be customized in three ways + (in order of precedence): + * Via `--qt-wrapper PyQt5` or `--qt-wrapper PyQt6` command-line arguments. + * Via the `QUTE_QT_WRAPPER` environment variable, set to `PyQt6` or `PyQt5`. + * For packagers wanting to provide packages specific to a Qt version, + patch `qutebrowser/qt/machinery.py` and set `_WRAPPER_OVERRIDE`. + Added ~~~~~ diff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec index 467994bab..1eee9161d 100644 --- a/misc/qutebrowser.spec +++ b/misc/qutebrowser.spec @@ -82,7 +82,7 @@ def get_data_files(): def get_hidden_imports(): - imports = [] if "PYINSTALLER_QT6" in os.environ else ['PyQt5.QtOpenGL'] + imports = ["PyQt5.QtOpenGL"] if "PYINSTALLER_QT5" in os.environ else [] for info in loader.walk_components(): imports.append(info.name) return imports diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 029fb4a6b..26f81ab23 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.15.9 -PyQt5-Qt5==5.15.2 -PyQt5-sip==12.12.1 -PyQtWebEngine==5.15.6 -PyQtWebEngine-Qt5==5.15.2 +PyQt6==6.5.1 +PyQt6-Qt6==6.5.1 +PyQt6-sip==13.5.1 +PyQt6-WebEngine==6.5.0 +PyQt6-WebEngine-Qt6==6.5.1 diff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw index 9c6afbf16..68a5db685 100644 --- a/misc/requirements/requirements-pyqt.txt-raw +++ b/misc/requirements/requirements-pyqt.txt-raw @@ -1,2 +1,4 @@ -PyQt5 -PyQtWebEngine +PyQt6 +PyQt6-Qt6 +PyQt6-WebEngine +PyQt6-WebEngine-Qt6 diff --git a/misc/userscripts/add-nextcloud-bookmarks b/misc/userscripts/add-nextcloud-bookmarks index 86f4f5bc7..2a480ccff 100755 --- a/misc/userscripts/add-nextcloud-bookmarks +++ b/misc/userscripts/add-nextcloud-bookmarks @@ -41,7 +41,7 @@ from json import dumps from os import environ, path from sys import argv, exit -from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit +from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit from requests import get, post from requests.auth import HTTPBasicAuth @@ -54,7 +54,7 @@ def get_text(name, info): None, "add-nextcloud-bookmarks userscript", "Please enter {}".format(info), - QLineEdit.Password, + QLineEdit.EchoMode.Password, ) else: text, ok = QInputDialog.getText( diff --git a/misc/userscripts/add-nextcloud-cookbook b/misc/userscripts/add-nextcloud-cookbook index 3952bb16f..151090785 100755 --- a/misc/userscripts/add-nextcloud-cookbook +++ b/misc/userscripts/add-nextcloud-cookbook @@ -37,7 +37,7 @@ import configparser from os import environ, path from sys import argv, exit -from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit +from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit from requests import post from requests.auth import HTTPBasicAuth @@ -50,7 +50,7 @@ def get_text(name, info): None, "add-nextcloud-cookbook userscript", "Please enter {}".format(info), - QLineEdit.Password, + QLineEdit.EchoMode.Password, ) else: text, ok = QInputDialog.getText( diff --git a/misc/userscripts/qute-keepass b/misc/userscripts/qute-keepass index 285377ffc..f88493d8e 100755 --- a/misc/userscripts/qute-keepass +++ b/misc/userscripts/qute-keepass @@ -42,7 +42,7 @@ you do not do this, you will get 'element not editable' errors. If keepass takes a while to open the DB, you might want to consider reducing the number of transform rounds in your database settings. -Dependencies: pykeepass (in python3), PyQt5. Without pykeepass, you will get an +Dependencies: pykeepass (in python3), PyQt6. Without pykeepass, you will get an exit code of 100. ********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!****************** @@ -64,8 +64,8 @@ import shlex import subprocess import sys -from PyQt5.QtCore import QUrl -from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit +from PyQt6.QtCore import QUrl +from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit try: import pykeepass @@ -152,7 +152,7 @@ def get_password(): text, ok = QInputDialog.getText( None, "KeePass DB Password", "Please enter your KeePass Master Password", - QLineEdit.Password) + QLineEdit.EchoMode.Password) if not ok: stderr('Password Prompt Rejected.') sys.exit(ExitCodes.USER_QUIT) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index bb2ff56e7..fbfa3df12 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -367,6 +367,14 @@ def _open_special_pages(args): os.environ.get("QTWEBENGINE_DISABLE_SANDBOX") == "1" ), 'qute://warning/sandboxing'), + + ('qt5-warning-shown', + ( + machinery.IS_QT5 and + machinery.INFO.reason == machinery.SelectionReason.auto and + objects.backend != usertypes.Backend.QtWebKit, + ), + 'qute://warning/qt5'), ] if 'quickstart-done' not in general_sect: diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 25834670b..0073f9bd2 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -22,6 +22,7 @@ Module attributes: _HANDLERS: The handlers registered via decorators. """ +import sys import html import json import os @@ -583,6 +584,12 @@ def qute_warning(url: QUrl) -> _HandlerRet: elif path == '/sandboxing': src = jinja.render('warning-sandboxing.html', title='Qt 6 macOS sandboxing warning') + elif path == '/qt5': + is_venv = hasattr(sys, 'real_prefix') or sys.base_prefix != sys.prefix + src = jinja.render('warning-qt5.html', + title='Switch to Qt 6', + is_venv=is_venv, + prefix=sys.prefix) else: raise NotFoundError("Invalid warning page {}".format(path)) return 'text/html', src diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index e55d75ecd..c2957181b 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -400,7 +400,8 @@ class WebEngineCaret(browsertab.AbstractCaret): # https://bugreports.qt.io/browse/QTBUG-53134 # Even on Qt 5.10 selectedText() seems to work poorly, see # https://github.com/qutebrowser/qutebrowser/issues/3523 - # FIXME:qt6 Reevaluate? + # With Qt 6.2-6.5, there still seem to be issues (especially with + # multi-line text) self._tab.run_js_async(javascript.assemble('caret', 'getSelection'), callback) diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index ef3e3bea5..8bf5031b1 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -15,16 +15,15 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """QtWebKit specific part of the web element API.""" from typing import cast, TYPE_CHECKING, Iterator, List, Optional, Set from qutebrowser.qt.core import QRect, Qt +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebElement, QWebSettings from qutebrowser.qt.webkitwidgets import QWebFrame +# pylint: enable=no-name-in-module from qutebrowser.config import config from qutebrowser.utils import log, utils, javascript, usertypes diff --git a/qutebrowser/browser/webkit/webkithistory.py b/qutebrowser/browser/webkit/webkithistory.py index aea648361..d89c705e6 100644 --- a/qutebrowser/browser/webkit/webkithistory.py +++ b/qutebrowser/browser/webkit/webkithistory.py @@ -15,14 +15,13 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """QtWebKit specific part of history.""" import functools +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebHistoryInterface +# pylint: enable=no-name-in-module from qutebrowser.utils import debug from qutebrowser.misc import debugcachestats diff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py index cb9cb5615..c181435d1 100644 --- a/qutebrowser/browser/webkit/webkitinspector.py +++ b/qutebrowser/browser/webkit/webkitinspector.py @@ -15,13 +15,13 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module """Customized QWebInspector for QtWebKit.""" +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebSettings from qutebrowser.qt.webkitwidgets import QWebInspector, QWebPage +# pylint: enable=no-name-in-module from qutebrowser.qt.widgets import QWidget from qutebrowser.browser import inspector diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index bd65be65b..a20811bae 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """Bridge from QWebSettings to our own settings. Module attributes: @@ -30,8 +27,10 @@ import os.path from qutebrowser.qt.core import QUrl from qutebrowser.qt.gui import QFont +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebSettings from qutebrowser.qt.webkitwidgets import QWebPage +# pylint: enable=no-name-in-module from qutebrowser.config import config, websettings from qutebrowser.config.websettings import AttributeInfo as Attr diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index a756e1a3d..e0483a23a 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module """Wrapper over our (QtWebKit) WebView.""" @@ -28,8 +26,10 @@ from typing import cast, Iterable, Optional from qutebrowser.qt.core import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize from qutebrowser.qt.gui import QIcon from qutebrowser.qt.widgets import QWidget +# pylint: disable=no-name-in-module from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame from qutebrowser.qt.webkit import QWebSettings, QWebHistory, QWebElement +# pylint: enable=no-name-in-module from qutebrowser.qt.printsupport import QPrinter from qutebrowser.browser import browsertab, shared diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 27429f331..b3b1b7ceb 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """The main browser widgets.""" import html @@ -28,7 +25,9 @@ from qutebrowser.qt.gui import QDesktopServices from qutebrowser.qt.network import QNetworkReply, QNetworkRequest from qutebrowser.qt.widgets import QFileDialog from qutebrowser.qt.printsupport import QPrintDialog +# pylint: disable=no-name-in-module from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame +# pylint: enable=no-name-in-module from qutebrowser.config import websettings, config from qutebrowser.browser import pdfjs, shared, downloads, greasemonkey diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 7a08a0736..831b2b689 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -15,14 +15,13 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """The main browser widgets.""" from qutebrowser.qt.core import pyqtSignal, Qt +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebSettings from qutebrowser.qt.webkitwidgets import QWebView, QWebPage +# pylint: enable=no-name-in-module from qutebrowser.config import config, stylesheet from qutebrowser.keyinput import modeman diff --git a/qutebrowser/html/warning-qt5.html b/qutebrowser/html/warning-qt5.html new file mode 100644 index 000000000..17af2f72c --- /dev/null +++ b/qutebrowser/html/warning-qt5.html @@ -0,0 +1,28 @@ +{% extends "styled.html" %} + +{% block content %} +<h1>{{ title }}</h1> +<span class="note">Note this warning will only appear once. Use <span class="mono">:open +qute://warning/qt5</span> to show it again at a later time.</span> + +<p> + qutebrowser <b>now supports Qt 6</b>. +</p> +<p> + However, in your environment, <b>Qt 6 is not installed</b>. Thus, qutebrowser is still using Qt 5 instead. + + Qt 5.15 based on a very old Chromium version (83 or 87, from mid/late 2020). +</p> +{% if is_venv %} +<p> + You are using a virtualenv. If you want to use Qt 6, you need to create a new + virtualenv with PyQt6 installed. + + If using <span class="mono">mkvenv.py</span>, <b>rerun the script</b> to create a + new virtualenv with Qt 6. +</p> +{% endif %} +<p> + <span class="note">Python installation prefix: <span class="mono">{{ prefix }}</span></span> +</p> +{% endblock %} diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index e2a15b2c0..10f4d5378 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -37,9 +37,7 @@ from qutebrowser.qt import machinery from qutebrowser.qt.core import Qt, QEvent from qutebrowser.qt.gui import QKeySequence, QKeyEvent if machinery.IS_QT6: - # FIXME:qt6 (lint) how come pylint isn't picking this up with both backends - # installed? - from qutebrowser.qt.core import QKeyCombination # pylint: disable=no-name-in-module + from qutebrowser.qt.core import QKeyCombination else: QKeyCombination = None # QKeyCombination was added in Qt 6 @@ -349,7 +347,7 @@ def _unset_modifier_bits( https://github.com/python/cpython/issues/105497 """ if machinery.IS_QT5: - return cast(_ModifierType, modifiers & ~mask) + return Qt.KeyboardModifiers(modifiers & ~mask) # can lose type if it's 0 else: return Qt.KeyboardModifier(modifiers.value & ~mask.value) @@ -369,11 +367,14 @@ class KeyInfo: def __post_init__(self) -> None: """Run some validation on the key/modifier values.""" - # This is mainly useful while porting from Qt 5 to 6. - # FIXME:qt6 do we want to remove or keep this (and fix the remaining - # issues) when done? - # assert isinstance(self.key, Qt.Key), self.key - # assert isinstance(self.modifiers, Qt.KeyboardModifier), self.modifiers + # This changed with Qt 6, and e.g. to_qt() relies on this. + if machinery.IS_QT5: + modifier_classes = (Qt.KeyboardModifier, Qt.KeyboardModifiers) + elif machinery.IS_QT6: + modifier_classes = Qt.KeyboardModifier + assert isinstance(self.key, Qt.Key), self.key + assert isinstance(self.modifiers, modifier_classes), self.modifiers + _assert_plain_key(self.key) _assert_plain_modifier(self.modifiers) @@ -488,16 +489,7 @@ class KeyInfo: if machinery.IS_QT5: return int(self.key) | int(self.modifiers) else: - try: - # FIXME:qt6 We might want to consider only supporting KeyInfo to be - # instanciated with a real Qt.Key, not with ints. See __post_init__. - key = Qt.Key(self.key) - except ValueError as e: - # WORKAROUND for - # https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html - raise InvalidKeyError(e) - - return QKeyCombination(self.modifiers, key) + return QKeyCombination(self.modifiers, self.key) def with_stripped_modifiers(self, modifiers: Qt.KeyboardModifier) -> "KeyInfo": mods = _unset_modifier_bits(self.modifiers, modifiers) diff --git a/qutebrowser/qt/_core_pyqtproperty.py b/qutebrowser/qt/_core_pyqtproperty.py index 8ae62264f..ae6435039 100644 --- a/qutebrowser/qt/_core_pyqtproperty.py +++ b/qutebrowser/qt/_core_pyqtproperty.py @@ -5,7 +5,7 @@ https://github.com/python-qt-tools/PyQt5-stubs/blob/5.15.6.0/PyQt5-stubs/QtCore. """ # flake8: noqa -# pylint: disable=invalid-name,missing-class-docstring,too-many-arguments,redefined-builtin,unused-argument,import-error +# pylint: disable=invalid-name,missing-class-docstring,too-many-arguments,redefined-builtin,unused-argument,no-name-in-module import typing from PyQt6.QtCore import QObject, pyqtSignal diff --git a/qutebrowser/qt/machinery.py b/qutebrowser/qt/machinery.py index e626edcb4..616c7ccfc 100644 --- a/qutebrowser/qt/machinery.py +++ b/qutebrowser/qt/machinery.py @@ -3,6 +3,16 @@ """Qt wrapper selection. Contains selection logic and globals for Qt wrapper selection. + +All other files in this package are intended to be simple wrappers around Qt imports. +Depending on what is set in this module, they import from PyQt5 or PyQt6. + +The import wrappers are intended to be as thin as possible. They will not unify +API-level differences between Qt 5 and Qt 6. This is best handled by the calling code, +which has a better picture of what changed between APIs and how to best handle it. + +What they *will* do is handle simple 1:1 renames of classes, or moves between +modules (where they aim to always expose the Qt 6 API). See e.g. webenginecore.py. """ # NOTE: No qutebrowser or PyQt import should be done here (at import time), @@ -20,11 +30,11 @@ from typing import Optional, Dict from qutebrowser.utils import log -# Packagers: Patch the line below to change the default wrapper for Qt 6 packages, e.g.: -# sed -i 's/_DEFAULT_WRAPPER = "PyQt5"/_DEFAULT_WRAPPER = "PyQt6"/' qutebrowser/qt/machinery.py +# Packagers: Patch the line below to enforce a Qt wrapper, e.g.: +# sed -i 's/_WRAPPER_OVERRIDE = .*/_WRAPPER_OVERRIDE = "PyQt6"/' qutebrowser/qt/machinery.py # # Users: Set the QUTE_QT_WRAPPER environment variable to change the default wrapper. -_DEFAULT_WRAPPER = "PyQt5" +_WRAPPER_OVERRIDE = None WRAPPERS = [ "PyQt6", @@ -80,6 +90,9 @@ class SelectionReason(enum.Enum): #: The wrapper was faked/patched out (e.g. in tests). fake = "fake" + #: The wrapper was overridden by patching _WRAPPER_OVERRIDE. + override = "override" + #: The reason was not set. unknown = "unknown" @@ -152,7 +165,7 @@ def _select_wrapper(args: Optional[argparse.Namespace]) -> SelectionInfo: - If --qt-wrapper is given, use that. - Otherwise, if the QUTE_QT_WRAPPER environment variable is set, use that. - - Otherwise, use PyQt5 (FIXME:qt6 autoselect). + - Otherwise, try the wrappers in WRAPPER in order (PyQt6 -> PyQt5) """ # If any Qt wrapper has been imported before this, something strange might # be happening. @@ -170,15 +183,17 @@ def _select_wrapper(args: Optional[argparse.Namespace]) -> SelectionInfo: if env_wrapper == "auto": return _autoselect_wrapper() elif env_wrapper not in WRAPPERS: - raise Error(f"Unknown wrapper {env_wrapper} set via {env_var}, " - f"allowed: {', '.join(WRAPPERS)}") + raise Error( + f"Unknown wrapper {env_wrapper} set via {env_var}, " + f"allowed: {', '.join(WRAPPERS)}" + ) return SelectionInfo(wrapper=env_wrapper, reason=SelectionReason.env) - # FIXME:qt6 Go back to the auto-detection once ready - # FIXME:qt6 Make sure to still consider _DEFAULT_WRAPPER for packagers - # (rename to _WRAPPER_OVERRIDE since our sed command is broken anyways then?) - # return _autoselect_wrapper() - return SelectionInfo(wrapper=_DEFAULT_WRAPPER, reason=SelectionReason.default) + if _WRAPPER_OVERRIDE is not None: + assert _WRAPPER_OVERRIDE in WRAPPERS # type: ignore[unreachable] + return SelectionInfo(wrapper=_WRAPPER_OVERRIDE, reason=SelectionReason.override) + + return _autoselect_wrapper() # Values are set in init(). If you see a NameError here, it means something tried to @@ -219,8 +234,7 @@ def _set_globals(info: SelectionInfo) -> None: Those are split into multiple global variables because that way we can teach mypy about them via --always-true and --always-false, see tox.ini. """ - global INFO, USE_PYQT5, USE_PYQT6, USE_PYSIDE6, IS_QT5, IS_QT6, \ - IS_PYQT, IS_PYSIDE, _initialized + global INFO, USE_PYQT5, USE_PYQT6, USE_PYSIDE6, IS_QT5, IS_QT6, IS_PYQT, IS_PYSIDE, _initialized assert info.wrapper is not None, info assert not _initialized diff --git a/qutebrowser/qt/opengl.py b/qutebrowser/qt/opengl.py index 0a14dffad..bc5a31c11 100644 --- a/qutebrowser/qt/opengl.py +++ b/qutebrowser/qt/opengl.py @@ -1,4 +1,4 @@ -# pylint: disable=import-error,wildcard-import,unused-import +# pylint: disable=import-error,wildcard-import,unused-import,unused-wildcard-import """Wrapped Qt imports for Qt OpenGL. diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index e778cc23a..fcca87feb 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -50,7 +50,7 @@ except ImportError: sys.exit(100) check_python_version() -import argparse # FIXME:qt6 (lint): disable=wrong-import-order +import argparse from qutebrowser.misc import earlyinit from qutebrowser.qt import machinery diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 996487693..d8f9693e7 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -136,7 +136,7 @@ def _smoke_test_run( return subprocess.run(argv, check=True, capture_output=True) -def smoke_test(executable: pathlib.Path, debug: bool, qt6: bool) -> None: +def smoke_test(executable: pathlib.Path, debug: bool, qt5: bool) -> None: """Try starting the given qutebrowser executable.""" stdout_whitelist = [] stderr_whitelist = [ @@ -176,7 +176,7 @@ def smoke_test(executable: pathlib.Path, debug: bool, qt6: bool) -> None: r'ContextResult::kTransientFailure: Failed to send ' r'.*CreateCommandBuffer\.'), ]) - if qt6: + if not qt5: stderr_whitelist.extend([ # FIXME:qt6 Qt 6.3 on macOS r'[0-9:]* WARNING: Incompatible version of OpenSSL', @@ -257,10 +257,10 @@ def verify_windows_exe(exe_path: pathlib.Path) -> None: assert pe.verify_checksum() -def patch_mac_app(qt6: bool) -> None: +def patch_mac_app(qt5: bool) -> None: """Patch .app to save some space and make it signable.""" dist_path = pathlib.Path('dist') - ver = '6' if qt6 else '5' + ver = '5' if qt5 else '6' app_path = dist_path / 'qutebrowser.app' contents_path = app_path / 'Contents' @@ -280,7 +280,7 @@ def patch_mac_app(qt6: bool) -> None: file_path.unlink() file_path.symlink_to(target) - if qt6: + if not qt5: # Symlinking QtWebEngineCore.framework does not seem to work with Qt 6. # Also, the symlinking/moving before signing doesn't seem to be required. return @@ -333,7 +333,7 @@ def _mac_bin_path(base: pathlib.Path) -> pathlib.Path: def build_mac( *, gh_token: Optional[str], - qt6: bool, + qt5: bool, skip_packaging: bool, debug: bool, ) -> List[Artifact]: @@ -348,20 +348,20 @@ def build_mac( shutil.rmtree(d, ignore_errors=True) utils.print_title("Updating 3rdparty content") - update_3rdparty.run(ace=False, pdfjs=True, legacy_pdfjs=not qt6, fancy_dmg=False, + update_3rdparty.run(ace=False, pdfjs=True, legacy_pdfjs=qt5, fancy_dmg=False, gh_token=gh_token) utils.print_title("Building .app via pyinstaller") - call_tox(f'pyinstaller-64bit{"-qt6" if qt6 else ""}', '-r', debug=debug) + call_tox(f'pyinstaller-64bit{"-qt5" if qt5 else ""}', '-r', debug=debug) utils.print_title("Patching .app") - patch_mac_app(qt6=qt6) + patch_mac_app(qt5=qt5) utils.print_title("Re-signing .app") sign_mac_app() dist_path = pathlib.Path("dist") utils.print_title("Running pre-dmg smoke test") - smoke_test(_mac_bin_path(dist_path), debug=debug, qt6=qt6) + smoke_test(_mac_bin_path(dist_path), debug=debug, qt5=qt5) if skip_packaging: return [] @@ -371,7 +371,7 @@ def build_mac( subprocess.run(['make', '-f', dmg_makefile_path], check=True) suffix = "-debug" if debug else "" - suffix += "-qt6" if qt6 else "" + suffix += "-qt5" if qt5 else "" dmg_path = dist_path / f'qutebrowser-{qutebrowser.__version__}{suffix}.dmg' pathlib.Path('qutebrowser.dmg').rename(dmg_path) @@ -383,7 +383,7 @@ def build_mac( subprocess.run(['hdiutil', 'attach', dmg_path, '-mountpoint', tmp_path], check=True) try: - smoke_test(_mac_bin_path(tmp_path), debug=debug, qt6=qt6) + smoke_test(_mac_bin_path(tmp_path), debug=debug, qt5=qt5) finally: print("Waiting 10s for dmg to be detachable...") time.sleep(10) @@ -422,7 +422,7 @@ def _get_windows_python_path(x64: bool) -> pathlib.Path: def _build_windows_single( *, x64: bool, - qt6: bool, + qt5: bool, skip_packaging: bool, debug: bool, ) -> List[Artifact]: @@ -437,9 +437,9 @@ def _build_windows_single( python = _get_windows_python_path(x64=x64) suffix = "64bit" if x64 else "32bit" - if qt6: + if qt5: # FIXME:qt6 does this regress 391623d5ec983ecfc4512c7305c4b7a293ac3872? - suffix += "-qt6" + suffix += "-qt5" call_tox(f'pyinstaller-{suffix}', '-r', python=python, debug=debug) out_pyinstaller = dist_path / "qutebrowser" @@ -450,7 +450,7 @@ def _build_windows_single( verify_windows_exe(exe_path) utils.print_title(f"Running {human_arch} smoke test") - smoke_test(exe_path, debug=debug, qt6=qt6) + smoke_test(exe_path, debug=debug, qt5=qt5) if skip_packaging: return [] @@ -463,7 +463,7 @@ def _build_windows_single( desc_arch=human_arch, desc_suffix='' if x64 else ' (only for 32-bit Windows!)', debug=debug, - qt6=qt6, + qt5=qt5, ) @@ -472,12 +472,12 @@ def build_windows( skip_packaging: bool, only_32bit: bool, only_64bit: bool, - qt6: bool, + qt5: bool, debug: bool, ) -> List[Artifact]: """Build windows executables/setups.""" utils.print_title("Updating 3rdparty content") - update_3rdparty.run(nsis=True, ace=False, pdfjs=True, legacy_pdfjs=not qt6, + update_3rdparty.run(nsis=True, ace=False, pdfjs=True, legacy_pdfjs=qt5, fancy_dmg=False, gh_token=gh_token) utils.print_title("Building Windows binaries") @@ -493,14 +493,14 @@ def build_windows( x64=True, skip_packaging=skip_packaging, debug=debug, - qt6=qt6, + qt5=qt5, ) - if not only_64bit and not qt6: + if not only_64bit and not qt5: artifacts += _build_windows_single( x64=False, skip_packaging=skip_packaging, debug=debug, - qt6=qt6, + qt5=qt5, ) return artifacts @@ -514,7 +514,7 @@ def _package_windows_single( desc_suffix: str, filename_arch: str, debug: bool, - qt6: bool, + qt5: bool, ) -> List[Artifact]: """Build the given installer/zip for windows.""" artifacts = [] @@ -532,8 +532,8 @@ def _package_windows_single( ] if debug: name_parts.append('debug') - if qt6: - name_parts.append('qt6') + if qt5: + name_parts.append('qt5') name = '-'.join(name_parts) + '.exe' artifacts.append(Artifact( @@ -552,8 +552,8 @@ def _package_windows_single( ] if debug: zip_name_parts.append('debug') - if qt6: - zip_name_parts.append('qt6') + if qt5: + zip_name_parts.append('qt5') zip_name = '-'.join(zip_name_parts) + '.zip' zip_path = dist_path / zip_name @@ -738,8 +738,8 @@ def main() -> None: help="Skip Windows 32 bit build.", dest='only_64bit') parser.add_argument('--debug', action='store_true', required=False, help="Build a debug build.") - parser.add_argument('--qt6', action='store_true', required=False, - help="Build against PyQt6") + parser.add_argument('--qt5', action='store_true', required=False, + help="Build against PyQt5") args = parser.parse_args() utils.change_cwd() @@ -768,14 +768,14 @@ def main() -> None: skip_packaging=args.skip_packaging, only_32bit=args.only_32bit, only_64bit=args.only_64bit, - qt6=args.qt6, + qt5=args.qt5, debug=args.debug, ) elif IS_MACOS: artifacts = build_mac( gh_token=gh_token, skip_packaging=args.skip_packaging, - qt6=args.qt6, + qt5=args.qt5, debug=args.debug, ) else: diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 240b5e6f1..215a1cfa0 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -27,7 +27,7 @@ import subprocess import tokenize import traceback import pathlib -from typing import List, Iterator, Optional +from typing import List, Iterator, Optional, Tuple REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO_ROOT)) @@ -152,6 +152,24 @@ def _check_spelling_file(path, fobj, patterns): return ok +def _check_spelling_all( + args: argparse.Namespace, + ignored: List[pathlib.Path], + patterns: List[Tuple[re.Pattern, str]], +) -> Optional[bool]: + try: + ok = True + for path in _get_files(verbose=args.verbose, ignored=ignored): + with tokenize.open(str(path)) as f: + if not _check_spelling_file(path, f, patterns): + ok = False + print() + return ok + except Exception: + traceback.print_exc() + return None + + def check_spelling(args: argparse.Namespace) -> Optional[bool]: """Check commonly misspelled words.""" # Words which I often misspell @@ -273,25 +291,13 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: hint_data / 'ace' / 'ace.js', hint_data / 'bootstrap' / 'bootstrap.css', ] - - try: - ok = True - for path in _get_files(verbose=args.verbose, ignored=ignored): - with tokenize.open(path) as f: - if not _check_spelling_file(path, f, patterns): - ok = False - print() - return ok - except Exception: - traceback.print_exc() - return None + return _check_spelling_all(args=args, ignored=ignored, patterns=patterns) def check_pyqt_imports(args: argparse.Namespace) -> Optional[bool]: """Check for direct PyQt imports.""" ignored = [ pathlib.Path("qutebrowser", "qt"), - # FIXME:qt6 fix those too? pathlib.Path("misc", "userscripts"), pathlib.Path("scripts"), ] @@ -305,18 +311,7 @@ def check_pyqt_imports(args: argparse.Namespace) -> Optional[bool]: "Use 'import qutebrowser.qt.MODULE' instead", ) ] - # FIXME:qt6 unify this with check_spelling somehow? - try: - ok = True - for path in _get_files(verbose=args.verbose, ignored=ignored): - with tokenize.open(str(path)) as f: - if not _check_spelling_file(path, f, patterns): - ok = False - print() - return ok - except Exception: - traceback.print_exc() - return None + return _check_spelling_all(args=args, ignored=ignored, patterns=patterns) def check_vcs_conflict(args: argparse.Namespace) -> Optional[bool]: diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 960b5a514..1e7ed0f61 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -60,7 +60,6 @@ def whitelist_generator(): # noqa: C901 yield 'qutebrowser.misc.sql.SqliteErrorCode.CONSTRAINT' yield 'qutebrowser.misc.throttle.Throttle.set_delay' yield 'qutebrowser.misc.guiprocess.GUIProcess.stderr' - yield 'qutebrowser.qt.machinery._autoselect_wrapper' # FIXME:qt6 # Qt attributes yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl' diff --git a/scripts/dev/standardpaths_tester.py b/scripts/dev/standardpaths_tester.py index ff85b2a4c..bbd0a39fb 100644 --- a/scripts/dev/standardpaths_tester.py +++ b/scripts/dev/standardpaths_tester.py @@ -21,7 +21,7 @@ import os import sys -from PyQt5.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion, +from PyQt6.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion, QStandardPaths, QCoreApplication) diff --git a/scripts/keytester.py b/scripts/keytester.py index 6d994114d..861133c06 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -21,8 +21,7 @@ Use python3 -m scripts.keytester to launch it. """ -from PyQt5.QtWidgets import QApplication - +from qutebrowser.qt.widgets import QApplication from qutebrowser.misc import miscwidgets app = QApplication([]) diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index 4581bef41..63bdde959 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -125,7 +125,7 @@ def get_lib_path(executable, name, required=True): raise ValueError("Unexpected output: {!r}".format(output)) -def link_pyqt(executable, venv_path, *, version='5'): +def link_pyqt(executable, venv_path, *, version): """Symlink the systemwide PyQt/sip into the venv. Args: diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py index 625cedd1a..9e9c1f4a2 100755 --- a/scripts/mkvenv.py +++ b/scripts/mkvenv.py @@ -134,8 +134,7 @@ def pyqt_versions() -> List[str]: def _is_qt6_version(version: str) -> bool: """Check if the given version is Qt 6.""" - # FIXME:qt6 Adjust once auto = Qt 6 - return version == "6" or version.startswith("6.") + return version in ["auto", "6"] or version.startswith("6.") def run_venv( @@ -228,7 +227,7 @@ def requirements_file(name: str) -> pathlib.Path: def pyqt_requirements_file(version: str) -> pathlib.Path: """Get the filename of the requirements file for the given PyQt version.""" - name = 'pyqt' if version == 'auto' else 'pyqt-{}'.format(version) + name = 'pyqt-6' if version == 'auto' else f'pyqt-{version}' return requirements_file(name) @@ -439,7 +438,7 @@ def run_qt_smoke_test_single( def run_qt_smoke_test(venv_dir: pathlib.Path, *, pyqt_version: str) -> None: """Make sure the Qt installation works.""" # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-104415 - no_debug = pyqt_version in ("6.3", "6") and sys.platform == "darwin" + no_debug = pyqt_version == "6.3" and sys.platform == "darwin" if no_debug: try: run_qt_smoke_test_single(venv_dir, debug=False, pyqt_version=pyqt_version) @@ -505,6 +504,9 @@ def install_pyqt(venv_dir, args): install_pyqt_binary(venv_dir, args.pyqt_version) if args.pyqt_snapshot: install_pyqt_shapshot(venv_dir, args.pyqt_snapshot.split(',')) + # Workaround until pyqt 6.5.2 is released on pypi + elif args.pyqt_version in ("6.5", "6", "auto"): + install_pyqt_shapshot(venv_dir, ["PyQt6-Qt6", "PyQt6-WebEngine-Qt6"]) elif args.pyqt_type == 'source': install_pyqt_source(venv_dir, args.pyqt_version) elif args.pyqt_type == 'link': diff --git a/scripts/opengl_info.py b/scripts/opengl_info.py index 5dc8f81c6..7c5ede6e7 100644 --- a/scripts/opengl_info.py +++ b/scripts/opengl_info.py @@ -18,8 +18,8 @@ """Show information about the OpenGL setup.""" -from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile, - QOffscreenSurface, QGuiApplication) +from PyQt6.QtGui import QOpenGLContext, QOffscreenSurface, QGuiApplication +from PyQt6.QtOpenGL import QOpenGLVersionProfile, QOpenGLVersionFunctionsFactory app = QGuiApplication([]) @@ -38,7 +38,7 @@ print(f"GLES: {ctx.isOpenGLES()}") vp = QOpenGLVersionProfile() vp.setVersion(2, 0) -vf = ctx.versionFunctions(vp) +vf = QOpenGLVersionFunctionsFactory.get(vp, ctx) print(f"Vendor: {vf.glGetString(vf.GL_VENDOR)}") print(f"Renderer: {vf.glGetString(vf.GL_RENDERER)}") print(f"Version: {vf.glGetString(vf.GL_VERSION)}") diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py index 047454e25..cd40af6e8 100644 --- a/tests/unit/browser/webkit/test_tabhistory.py +++ b/tests/unit/browser/webkit/test_tabhistory.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """Tests for webelement.tabhistory.""" import dataclasses @@ -26,7 +23,9 @@ from typing import Any import pytest pytest.importorskip('qutebrowser.qt.webkit') from qutebrowser.qt.core import QUrl, QPoint +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebHistory +# pylint: enable=no-name-in-module from qutebrowser.browser.webkit import tabhistory from qutebrowser.misc.sessions import TabHistoryItem as Item diff --git a/tests/unit/javascript/test_js_execution.py b/tests/unit/javascript/test_js_execution.py index 542b56975..fd2469148 100644 --- a/tests/unit/javascript/test_js_execution.py +++ b/tests/unit/javascript/test_js_execution.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """Check how Qt behaves when trying to execute JS.""" @@ -29,7 +26,7 @@ def test_simple_js_webkit(webview, js_enabled, expected): """With QtWebKit, evaluateJavaScript works when JS is on.""" # If we get there (because of the webview fixture) we can be certain # QtWebKit is available - from qutebrowser.qt.webkit import QWebSettings + from qutebrowser.qt.webkit import QWebSettings # pylint: disable=no-name-in-module webview.settings().setAttribute(QWebSettings.WebAttribute.JavascriptEnabled, js_enabled) result = webview.page().mainFrame().evaluateJavaScript('1 + 1') assert result == expected @@ -40,7 +37,7 @@ def test_element_js_webkit(webview, js_enabled, expected): """With QtWebKit, evaluateJavaScript on an element works with JS off.""" # If we get there (because of the webview fixture) we can be certain # QtWebKit is available - from qutebrowser.qt.webkit import QWebSettings + from qutebrowser.qt.webkit import QWebSettings # pylint: disable=no-name-in-module webview.settings().setAttribute(QWebSettings.WebAttribute.JavascriptEnabled, js_enabled) elem = webview.page().mainFrame().documentElement() result = elem.evaluateJavaScript('1 + 1') diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index 3826d3ee9..5f151704a 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -24,6 +24,7 @@ import dataclasses from typing import Optional from qutebrowser.qt.core import Qt +from qutebrowser.keyinput import keyutils @dataclasses.dataclass(order=True) @@ -606,7 +607,7 @@ KEYS = [ Key('unknown', 'Unknown', qtest=False), # 0x0 is used by Qt for unknown keys... - Key(attribute='', name='nil', member=0x0, qtest=False), + Key(attribute='', name='nil', member=keyutils._NIL_KEY, qtest=False), ] diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 52e0a01df..0ae0702e9 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -171,7 +171,7 @@ class TestHandle: assert not prompt_keyparser._count def test_invalid_key(self, prompt_keyparser): - keys = [Qt.Key.Key_B, 0x0] + keys = [Qt.Key.Key_B, keyutils._NIL_KEY] for key in keys: info = keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier) prompt_keyparser.handle(info.to_event()) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 2c0740c20..c3b6dc236 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -31,6 +31,16 @@ from qutebrowser.keyinput import keyutils from qutebrowser.utils import utils +pyqt_enum_workaround_skip = pytest.mark.skipif( + isinstance(keyutils._NIL_KEY, int), + reason="Can't create QKey for unknown keys with this PyQt version" +) +try: + OE_KEY = Qt.Key(ord('Œ')) +except ValueError: + OE_KEY = None # affected tests skipped + + @pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute) def qt_key(request): """Get all existing keys from key_data.py. @@ -156,10 +166,14 @@ class TestKeyToString: (Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier | Qt.KeyboardModifier.ShiftModifier, '<Meta+Ctrl+Alt+Shift+a>'), - (ord('Œ'), Qt.KeyboardModifier.NoModifier, '<Œ>'), - (ord('Œ'), Qt.KeyboardModifier.ShiftModifier, '<Shift+Œ>'), - (ord('Œ'), Qt.KeyboardModifier.GroupSwitchModifier, '<AltGr+Œ>'), - (ord('Œ'), Qt.KeyboardModifier.GroupSwitchModifier | Qt.KeyboardModifier.ShiftModifier, '<AltGr+Shift+Œ>'), + + pytest.param(OE_KEY, Qt.KeyboardModifier.NoModifier, '<Œ>', + marks=pyqt_enum_workaround_skip), + pytest.param(OE_KEY, Qt.KeyboardModifier.ShiftModifier, '<Shift+Œ>', + marks=pyqt_enum_workaround_skip), + pytest.param(OE_KEY, Qt.KeyboardModifier.GroupSwitchModifier, '<AltGr+Œ>', + marks=pyqt_enum_workaround_skip), + pytest.param(OE_KEY, Qt.KeyboardModifier.GroupSwitchModifier | Qt.KeyboardModifier.ShiftModifier, '<AltGr+Shift+Œ>'), (Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier, '<Shift>'), (Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, '<Ctrl+Shift>'), @@ -212,10 +226,10 @@ def test_surrogates(key, modifiers, text, expected, pyqt_enum_workaround): ([Qt.Key.Key_Shift, 0x29df6], '<Shift><𩷶>'), ([0x1f468, 0x200d, 0x1f468, 0x200d, 0x1f466], '<👨><><👨><><👦>'), ]) -def test_surrogate_sequences(keys, expected, pyqt_enum_workaround): - infos = [keyutils.KeyInfo(key) for key in keys] - with pyqt_enum_workaround(keyutils.KeyParseError): - seq = keyutils.KeySequence(*infos) +@pyqt_enum_workaround_skip +def test_surrogate_sequences(keys, expected): + infos = [keyutils.KeyInfo(Qt.Key(key)) for key in keys] + seq = keyutils.KeySequence(*infos) assert str(seq) == expected @@ -590,7 +604,8 @@ def test_key_info_to_qt(): (Qt.Key.Key_Return, False), (Qt.Key.Key_Enter, False), (Qt.Key.Key_Space, False), - (0x0, False), # Used by Qt for unknown keys + # Used by Qt for unknown keys + pytest.param(keyutils._NIL_KEY, False, marks=pyqt_enum_workaround_skip), (Qt.Key.Key_ydiaeresis, True), (Qt.Key.Key_X, True), diff --git a/tests/unit/test_qt_machinery.py b/tests/unit/test_qt_machinery.py index 1618e9e6f..32f63043b 100644 --- a/tests/unit/test_qt_machinery.py +++ b/tests/unit/test_qt_machinery.py @@ -23,6 +23,7 @@ import html import argparse import typing from typing import Any, Optional, List, Dict, Union +import dataclasses import pytest @@ -214,7 +215,7 @@ def modules(): reason=machinery.SelectionReason.auto, outcomes={ "PyQt6": "ImportError: Fake ImportError for PyQt6.", - } + }, ), id="import-error", ), @@ -230,111 +231,157 @@ def test_autoselect( assert machinery._autoselect_wrapper() == expected -@pytest.mark.parametrize( - "args, env, expected", - [ - # Defaults with no overrides - ( - None, - None, - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.default +@dataclasses.dataclass +class SelectWrapperCase: + name: str + expected: machinery.SelectionInfo + args: Optional[argparse.Namespace] = None + env: Optional[str] = None + override: Optional[str] = None + + def __str__(self): + return self.name + + +class TestSelectWrapper: + @pytest.mark.parametrize( + "tc", + [ + # Only argument given + SelectWrapperCase( + "pyqt6-arg", + args=argparse.Namespace(qt_wrapper="PyQt6"), + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.cli + ), ), - ), - ( - argparse.Namespace(qt_wrapper=None), - None, - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.default + SelectWrapperCase( + "pyqt5-arg", + args=argparse.Namespace(qt_wrapper="PyQt5"), + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.cli + ), ), - ), - ( - argparse.Namespace(qt_wrapper=None), - "", - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.default + SelectWrapperCase( + "pyqt6-arg-empty-env", + args=argparse.Namespace(qt_wrapper="PyQt5"), + env="", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.cli + ), ), - ), - # Only argument given - ( - argparse.Namespace(qt_wrapper="PyQt6"), - None, - machinery.SelectionInfo( - wrapper="PyQt6", reason=machinery.SelectionReason.cli + # Only environment variable given + SelectWrapperCase( + "pyqt6-env", + env="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.env + ), ), - ), - ( - argparse.Namespace(qt_wrapper="PyQt5"), - None, - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.cli + SelectWrapperCase( + "pyqt5-env", + env="PyQt5", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.env + ), ), - ), - ( - argparse.Namespace(qt_wrapper="PyQt5"), - "", - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.cli + # Both given + SelectWrapperCase( + "pyqt5-arg-pyqt6-env", + args=argparse.Namespace(qt_wrapper="PyQt5"), + env="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.cli + ), ), - ), - # Only environment variable given - ( - None, - "PyQt6", - machinery.SelectionInfo( - wrapper="PyQt6", reason=machinery.SelectionReason.env + SelectWrapperCase( + "pyqt6-arg-pyqt5-env", + args=argparse.Namespace(qt_wrapper="PyQt6"), + env="PyQt5", + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.cli + ), ), - ), - ( - None, - "PyQt5", - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.env + SelectWrapperCase( + "pyqt6-arg-pyqt6-env", + args=argparse.Namespace(qt_wrapper="PyQt6"), + env="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.cli + ), ), - ), - # Both given - ( - argparse.Namespace(qt_wrapper="PyQt5"), - "PyQt6", - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.cli + # Override + SelectWrapperCase( + "override-only", + override="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.override + ), ), - ), - ( - argparse.Namespace(qt_wrapper="PyQt6"), - "PyQt5", - machinery.SelectionInfo( - wrapper="PyQt6", reason=machinery.SelectionReason.cli + SelectWrapperCase( + "override-arg", + args=argparse.Namespace(qt_wrapper="PyQt5"), + override="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.cli + ), ), - ), - ( - argparse.Namespace(qt_wrapper="PyQt6"), - "PyQt6", - machinery.SelectionInfo( - wrapper="PyQt6", reason=machinery.SelectionReason.cli + SelectWrapperCase( + "override-env", + env="PyQt5", + override="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.env + ), ), - ), - ], -) -def test_select_wrapper( - args: Optional[argparse.Namespace], - env: Optional[str], - expected: machinery.SelectionInfo, - monkeypatch: pytest.MonkeyPatch, - undo_init: None, -): - if env is None: - monkeypatch.delenv("QUTE_QT_WRAPPER", raising=False) - else: - monkeypatch.setenv("QUTE_QT_WRAPPER", env) + ], + ids=str, + ) + def test_select(self, tc: SelectWrapperCase, monkeypatch: pytest.MonkeyPatch): + if tc.env is None: + monkeypatch.delenv("QUTE_QT_WRAPPER", raising=False) + else: + monkeypatch.setenv("QUTE_QT_WRAPPER", tc.env) + + if tc.override is not None: + monkeypatch.setattr(machinery, "_WRAPPER_OVERRIDE", tc.override) + + assert machinery._select_wrapper(tc.args) == tc.expected + + @pytest.mark.parametrize( + "args, env", + [ + (None, None), + (argparse.Namespace(qt_wrapper=None), None), + (argparse.Namespace(qt_wrapper=None), ""), + ], + ) + def test_autoselect_by_default( + self, + args: Optional[argparse.Namespace], + env: Optional[str], + monkeypatch: pytest.MonkeyPatch, + ): + """Test that the default behavior is to autoselect a wrapper. + + Autoselection itself is tested further down. + """ + if env is None: + monkeypatch.delenv("QUTE_QT_WRAPPER", raising=False) + else: + monkeypatch.setenv("QUTE_QT_WRAPPER", env) - assert machinery._select_wrapper(args) == expected + assert machinery._select_wrapper(args).reason == machinery.SelectionReason.auto + def test_after_qt_import(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setitem(sys.modules, "PyQt6", None) + with pytest.warns(UserWarning, match="PyQt6 already imported"): + machinery._select_wrapper(args=None) -def test_select_wrapper_after_qt_import(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setitem(sys.modules, "PyQt6", None) - with pytest.warns(UserWarning, match="PyQt6 already imported"): - machinery._select_wrapper(args=None) + def test_invalid_override(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(machinery, "_WRAPPER_OVERRIDE", "invalid") + with pytest.raises(AssertionError): + machinery._select_wrapper(args=None) class TestInit: @@ -359,15 +406,34 @@ class TestInit: ): machinery.init(args=empty_args) + @pytest.fixture(params=["auto", "", None]) + def qt_auto_env( + self, + request: pytest.FixtureRequest, + monkeypatch: pytest.MonkeyPatch, + ): + """Trigger wrapper autoselection via environment variable. + + Autoselection should be used in three scenarios: + + - The environment variable is set to "auto". + - The environment variable is set to an empty string. + - The environment variable is not set at all. + + We run test_none_available_*() for all three scenarios. + """ + if request.param is None: + monkeypatch.delenv("QUTE_QT_WRAPPER", raising=False) + else: + monkeypatch.setenv("QUTE_QT_WRAPPER", request.param) + def test_none_available_implicit( self, stubs: Any, modules: Dict[str, bool], monkeypatch: pytest.MonkeyPatch, - undo_init: None, + qt_auto_env: None, ): - # FIXME:qt6 Also try without this once auto is default - monkeypatch.setenv("QUTE_QT_WRAPPER", "auto") stubs.ImportFake(modules, monkeypatch).patch() message_lines = [ @@ -391,10 +457,8 @@ class TestInit: modules: Dict[str, bool], monkeypatch: pytest.MonkeyPatch, empty_args: argparse.Namespace, - undo_init: None, + qt_auto_env: None, ): - # FIXME:qt6 Also try without this once auto is default - monkeypatch.setenv("QUTE_QT_WRAPPER", "auto") stubs.ImportFake(modules, monkeypatch).patch() info = machinery.init(args=empty_args) @@ -403,7 +467,7 @@ class TestInit: reason=machinery.SelectionReason.auto, outcomes={ "PyQt6": "ImportError: Fake ImportError for PyQt6.", - } + }, ) @pytest.mark.parametrize( @@ -422,7 +486,6 @@ class TestInit: true_vars: str, explicit: bool, empty_args: argparse.Namespace, - undo_init: None, ): info = machinery.SelectionInfo( wrapper=selected_wrapper, @@ -11,10 +11,10 @@ minversion = 3.20 [testenv] setenv = - PYTEST_QT_API=pyqt5 - QUTE_QT_WRAPPER=PyQt5 - pyqt{62,63,64,65}: PYTEST_QT_API=pyqt6 - pyqt{62,63,64,65}: QUTE_QT_WRAPPER=PyQt6 + PYTEST_QT_API=pyqt6 + QUTE_QT_WRAPPER=PyQt6 + pyqt{515,5152}: PYTEST_QT_API=pyqt5 + pyqt{515,5152}: QUTE_QT_WRAPPER=PyQt5 cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= py312: VIRTUALENV_PIP=23.2 py312: PIP_REQUIRE_VIRTUALENV=0 @@ -56,10 +56,10 @@ commands = {envpython} -bb -m pytest {posargs:tests} cov: {envpython} scripts/dev/check_coverage.py {posargs} -[testenv:py-qt6] +[testenv:py-qt5] setenv = - PYTEST_QT_API=pyqt6 - QUTE_QT_WRAPPER=PyQt6 + PYTEST_QT_API=pyqt5 + QUTE_QT_WRAPPER=PyQt5 [testenv:bleeding] basepython = {env:PYTHON:python3} @@ -112,6 +112,7 @@ deps = -r{toxinidir}/misc/requirements/requirements-tests.txt -r{toxinidir}/misc/requirements/requirements-pylint.txt -r{toxinidir}/misc/requirements/requirements-pyqt.txt + -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt commands = {envpython} -m pylint scripts qutebrowser --output-format=colorized --reports=no {posargs} {envpython} scripts/dev/run_pylint_on_tests.py {toxinidir} --output-format=colorized --reports=no {posargs} @@ -180,19 +181,19 @@ commands = {envpython} scripts/dev/check_doc_changes.py {posargs} {envpython} scripts/asciidoc2html.py {posargs} -[testenv:pyinstaller-{64bit,32bit}{,-qt6}] +[testenv:pyinstaller-{64bit,32bit}{,-qt5}] basepython = {env:PYTHON:python3} passenv = APPDATA HOME PYINSTALLER_DEBUG setenv = - qt6: PYINSTALLER_QT6=true + qt5: PYINSTALLER_QT5=true deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt - !qt6: -r{toxinidir}/misc/requirements/requirements-pyqt.txt - qt6: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt + !qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt + qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt commands = {envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec @@ -246,9 +247,7 @@ commands = basepython = {env:PYTHON:python3} passenv = {[testenv:mypy-pyqt6]passenv} deps = {[testenv:mypy-pyqt6]deps} -setenv = - pyqt6: QUTE_CONSTANTS_ARGS=--always-true=USE_PYQT6 --always-false=USE_PYQT5 --always-false=USE_PYSIDE2 --always-false=USE_PYSIDE6 --always-false=IS_QT5 --always-true=IS_QT6 - pyqt5: QUTE_CONSTANTS_ARGS=--always-false=USE_PYQT6 --always-true=USE_PYQT5 --always-false=USE_PYSIDE2 --always-false=USE_PYSIDE6 --always-true=IS_QT5 --always-false=IS_QT6 +setenv = {[testenv:mypy-pyqt6]setenv} commands = {envpython} -m mypy --cobertura-xml-report {envtmpdir} {env:QUTE_CONSTANTS_ARGS} qutebrowser tests {posargs} {envdir}/bin/diff-cover --fail-under=100 --compare-branch={env:DIFF_BRANCH:origin/{env:GITHUB_BASE_REF:master}} {envtmpdir}/cobertura.xml @@ -264,21 +263,21 @@ deps = commands = {envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/ -[testenv:build-release{,-qt6}] +[testenv:build-release{,-qt5}] basepython = {env:PYTHON:python3} passenv = * -# Override default PyQt5 from [testenv] +# Override default PyQt6 from [testenv] setenv = - qt6: QUTE_QT_WRAPPER=PyQt6 + qt5: QUTE_QT_WRAPPER=PyQt5 usedevelop = true deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-tox.txt -r{toxinidir}/misc/requirements/requirements-docs.txt - !qt6: -r{toxinidir}/misc/requirements/requirements-pyqt.txt - qt6: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt + !qt5: -r{toxinidir}/misc/requirements/requirements-pyqt.txt + qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt -r{toxinidir}/misc/requirements/requirements-dev.txt -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt commands = - !qt6: {envpython} {toxinidir}/scripts/dev/build_release.py {posargs} - qt6: {envpython} {toxinidir}/scripts/dev/build_release.py --qt6 {posargs} + !qt5: {envpython} {toxinidir}/scripts/dev/build_release.py {posargs} + qt5: {envpython} {toxinidir}/scripts/dev/build_release.py --qt5 {posargs} |