Merge remote-tracking branch 'origin/pr/7789'

This commit is contained in:
Florian Bruhin 2023-07-22 12:36:31 +02:00
commit 273230eb07
42 changed files with 462 additions and 334 deletions

View File

@ -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]')"

View File

@ -15,6 +15,7 @@ jobs:
- archlinux-webkit
- archlinux-webengine
- archlinux-webengine-unstable
- archlinux-webengine-unstable-qt6
- archlinux-webengine-qt6
steps:
- uses: actions/checkout@v3

View File

@ -16,50 +16,50 @@ jobs:
include:
- os: macos-11
branch: master
toxenv: build-release-qt5
name: qt5-macos
- os: windows-2019
args: --64bit
branch: master
toxenv: build-release-qt5
name: qt5-windows-64bit
- os: windows-2019
args: --32bit
branch: master
toxenv: build-release-qt5
name: qt5-windows-32bit
- os: macos-11
args: --debug
branch: master
toxenv: build-release-qt5
name: qt5-macos-debug
- os: windows-2019
args: --64bit --debug
branch: master
toxenv: build-release-qt5
name: qt5-windows-64bit-debug
- os: windows-2019
args: --32bit --debug
branch: master
toxenv: build-release-qt5
name: qt5-windows-32bit-debug
- os: macos-11
toxenv: build-release
name: macos
- os: windows-2019
args: --64bit
branch: master
toxenv: build-release
name: windows-64bit
- os: windows-2019
args: --32bit
branch: master
toxenv: build-release
name: windows-32bit
- os: macos-11
args: --debug
branch: master
toxenv: build-release
name: macos-debug
- os: windows-2019
args: --64bit --debug
branch: master
toxenv: build-release
name: windows-64bit-debug
- os: windows-2019
args: --32bit --debug
branch: master
toxenv: build-release
name: windows-32bit-debug
- os: macos-11
toxenv: build-release-qt6
name: qt6-macos
- os: windows-2019
args: --64bit
toxenv: build-release-qt6
name: qt6-windows-64bit
- os: macos-11
args: --debug
toxenv: build-release-qt6
name: qt6-macos-debug
- os: windows-2019
args: --64bit --debug
toxenv: build-release-qt6
name: qt6-windows-64bit-debug
runs-on: "${{ matrix.os }}"
timeout-minutes: 45
steps:

View File

@ -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}$

View File

@ -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
~~~~~

View File

@ -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

View File

@ -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

View File

@ -1,2 +1,4 @@
PyQt5
PyQtWebEngine
PyQt6
PyQt6-Qt6
PyQt6-WebEngine
PyQt6-WebEngine-Qt6

View File

@ -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(

View File

@ -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(

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 %}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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:

View File

@ -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]:

View File

@ -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'

View File

@ -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)

View File

@ -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([])

View File

@ -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:

View File

@ -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':

View File

@ -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)}")

View File

@ -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

View File

@ -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')

View File

@ -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),
]

View File

@ -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())

View File

@ -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),

View File

@ -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
),
),
(
argparse.Namespace(qt_wrapper=None),
None,
machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.default
),
),
(
argparse.Namespace(qt_wrapper=None),
"",
machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.default
),
),
# Only argument given
(
argparse.Namespace(qt_wrapper="PyQt6"),
None,
machinery.SelectionInfo(
wrapper="PyQt6", reason=machinery.SelectionReason.cli
),
),
(
argparse.Namespace(qt_wrapper="PyQt5"),
None,
machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.cli
),
),
(
argparse.Namespace(qt_wrapper="PyQt5"),
"",
machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.cli
),
),
# Only environment variable given
(
None,
"PyQt6",
machinery.SelectionInfo(
wrapper="PyQt6", reason=machinery.SelectionReason.env
),
),
(
None,
"PyQt5",
machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.env
),
),
# Both given
(
argparse.Namespace(qt_wrapper="PyQt5"),
"PyQt6",
machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.cli
),
),
(
argparse.Namespace(qt_wrapper="PyQt6"),
"PyQt5",
machinery.SelectionInfo(
wrapper="PyQt6", reason=machinery.SelectionReason.cli
),
),
(
argparse.Namespace(qt_wrapper="PyQt6"),
"PyQt6",
machinery.SelectionInfo(
wrapper="PyQt6", reason=machinery.SelectionReason.cli
),
),
],
)
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)
@dataclasses.dataclass
class SelectWrapperCase:
name: str
expected: machinery.SelectionInfo
args: Optional[argparse.Namespace] = None
env: Optional[str] = None
override: Optional[str] = None
assert machinery._select_wrapper(args) == expected
def __str__(self):
return self.name
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)
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
),
),
SelectWrapperCase(
"pyqt5-arg",
args=argparse.Namespace(qt_wrapper="PyQt5"),
expected=machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.cli
),
),
SelectWrapperCase(
"pyqt6-arg-empty-env",
args=argparse.Namespace(qt_wrapper="PyQt5"),
env="",
expected=machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.cli
),
),
# Only environment variable given
SelectWrapperCase(
"pyqt6-env",
env="PyQt6",
expected=machinery.SelectionInfo(
wrapper="PyQt6", reason=machinery.SelectionReason.env
),
),
SelectWrapperCase(
"pyqt5-env",
env="PyQt5",
expected=machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.env
),
),
# Both given
SelectWrapperCase(
"pyqt5-arg-pyqt6-env",
args=argparse.Namespace(qt_wrapper="PyQt5"),
env="PyQt6",
expected=machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.cli
),
),
SelectWrapperCase(
"pyqt6-arg-pyqt5-env",
args=argparse.Namespace(qt_wrapper="PyQt6"),
env="PyQt5",
expected=machinery.SelectionInfo(
wrapper="PyQt6", reason=machinery.SelectionReason.cli
),
),
SelectWrapperCase(
"pyqt6-arg-pyqt6-env",
args=argparse.Namespace(qt_wrapper="PyQt6"),
env="PyQt6",
expected=machinery.SelectionInfo(
wrapper="PyQt6", reason=machinery.SelectionReason.cli
),
),
# Override
SelectWrapperCase(
"override-only",
override="PyQt6",
expected=machinery.SelectionInfo(
wrapper="PyQt6", reason=machinery.SelectionReason.override
),
),
SelectWrapperCase(
"override-arg",
args=argparse.Namespace(qt_wrapper="PyQt5"),
override="PyQt6",
expected=machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.cli
),
),
SelectWrapperCase(
"override-env",
env="PyQt5",
override="PyQt6",
expected=machinery.SelectionInfo(
wrapper="PyQt5", reason=machinery.SelectionReason.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).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_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,

41
tox.ini
View File

@ -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}