summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoofar <toofar@spalge.com>2022-09-16 16:58:49 +1200
committerGitHub <noreply@github.com>2022-09-16 16:58:49 +1200
commitbe5b4c5dd0d137cfbd00e57608787350df9eae67 (patch)
tree939801db4cd3bed3ce8373be383488ff2b2ab49e
parent8decafefbe94c111c1cd1b59aeb0b314cb4de5c9 (diff)
parent71e9b9d854f98627dcec378ed2b0b696177d66ba (diff)
downloadqutebrowser-be5b4c5dd0d137cfbd00e57608787350df9eae67.tar.gz
qutebrowser-be5b4c5dd0d137cfbd00e57608787350df9eae67.zip
Merge pull request #7392 from qutebrowser/feat/turn_on_mypy_pyqt5
Get mypy working, with pyqt5, on qt6-v2 branch
-rw-r--r--.github/workflows/ci.yml3
-rw-r--r--misc/requirements/requirements-mypy.txt2
-rw-r--r--qutebrowser/browser/browsertab.py15
-rw-r--r--qutebrowser/browser/hints.py2
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py5
-rw-r--r--qutebrowser/browser/webelem.py8
-rw-r--r--qutebrowser/browser/webengine/certificateerror.py77
-rw-r--r--qutebrowser/browser/webengine/darkmode.py2
-rw-r--r--qutebrowser/browser/webengine/notification.py6
-rw-r--r--qutebrowser/browser/webengine/webenginedownloads.py11
-rw-r--r--qutebrowser/browser/webengine/webengineinspector.py11
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py17
-rw-r--r--qutebrowser/browser/webengine/webview.py9
-rw-r--r--qutebrowser/keyinput/keyutils.py74
-rw-r--r--qutebrowser/mainwindow/mainwindow.py3
-rw-r--r--qutebrowser/mainwindow/prompt.py2
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py2
-rw-r--r--qutebrowser/misc/sql.py11
-rw-r--r--qutebrowser/qt/sip.py11
-rw-r--r--qutebrowser/utils/debug.py6
-rw-r--r--qutebrowser/utils/qtutils.py19
-rw-r--r--qutebrowser/utils/usertypes.py4
-rw-r--r--qutebrowser/utils/version.py31
-rw-r--r--tox.ini18
24 files changed, 181 insertions, 168 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bd4a223a9..d38b995c5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,8 @@ jobs:
- testenv: pylint
- testenv: flake8
# FIXME:qt6 (lint)
- # - testenv: mypy
+ # - testenv: mypy-pyqt6
+ - testenv: mypy-pyqt5
- testenv: docs
- testenv: vulture
- testenv: misc
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index d40954835..a515e91cc 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -12,6 +12,8 @@ mypy-extensions==0.4.3
pluggy==1.0.0
Pygments==2.13.0
PyQt5-stubs==5.15.6.0
+PyQt6==6.3.1
+PyQt6-WebEngine==6.3.1
tomli==2.0.1
types-PyYAML==6.0.11
typing_extensions==4.3.0
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 811a530a7..9c6c52e0e 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -236,12 +236,12 @@ class AbstractPrinting(QObject):
super().__init__(parent)
self._widget = cast(_WidgetType, None)
self._tab = tab
- self._dialog: QPrintDialog = None
+ self._dialog: Optional[QPrintDialog] = None
self.printing_finished.connect(self._on_printing_finished)
self.pdf_printing_finished.connect(self._on_pdf_printing_finished)
@pyqtSlot(bool)
- def _on_printing_finished(self, ok):
+ def _on_printing_finished(self, ok: bool) -> None:
# Only reporting error here, as the user has feedback from the dialog
# (and probably their printer) already.
if not ok:
@@ -251,7 +251,7 @@ class AbstractPrinting(QObject):
self._dialog = None
@pyqtSlot(str, bool)
- def _on_pdf_printing_finished(self, path, ok):
+ def _on_pdf_printing_finished(self, path: str, ok: bool) -> None:
if ok:
message.info(f"Printed to {path}")
else:
@@ -277,7 +277,7 @@ class AbstractPrinting(QObject):
"""Print the tab to a PDF with the given filename."""
raise NotImplementedError
- def to_printer(self, printer: QPrinter):
+ def to_printer(self, printer: QPrinter) -> None:
"""Print the tab.
Args:
@@ -288,7 +288,9 @@ class AbstractPrinting(QObject):
def show_dialog(self) -> None:
"""Print with a QPrintDialog."""
self._dialog = QPrintDialog(self._tab)
- self._dialog.open(lambda: self.to_printer(self._dialog.printer()))
+ assert self._dialog is not None
+ not_none_dialog = self._dialog
+ self._dialog.open(lambda: self.to_printer(not_none_dialog.printer()))
# Gets cleaned up in on_printing_finished
@@ -1320,7 +1322,8 @@ class AbstractTab(QWidget):
def __repr__(self) -> str:
try:
qurl = self.url()
- url = qurl.toDisplayString(QUrl.ComponentFormattingOption.EncodeUnicode)
+ as_unicode = QUrl.ComponentFormattingOption.EncodeUnicode
+ url = qurl.toDisplayString(as_unicode) # type: ignore[arg-type]
except (AttributeError, RuntimeError) as exc:
url = '<{}>'.format(exc.__class__.__name__)
else:
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 91534a58b..8fe75f271 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -254,7 +254,7 @@ class HintActions:
flags = QUrl.ComponentFormattingOption.FullyEncoded | QUrl.UrlFormattingOption.RemovePassword
if url.scheme() == 'mailto':
- flags |= QUrl.UrlFormattingOption.RemoveScheme
+ flags |= QUrl.UrlFormattingOption.RemoveScheme # type: ignore[operator]
urlstr = url.toString(flags)
new_content = urlstr
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index 586570390..242565a39 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -177,7 +177,10 @@ class DownloadItem(downloads.AbstractDownloadItem):
@pyqtSlot(QUrl)
def _on_redirected(self, url):
- log.downloads.debug(f"redirected: {self._reply.url()} -> {url}")
+ if self._reply is None:
+ log.downloads.warning(f"redirected: REPLY GONE -> {url}")
+ else:
+ log.downloads.debug(f"redirected: {self._reply.url()} -> {url}")
def _do_cancel(self):
self._read_timer.stop()
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index a734f15b8..91e82ae0c 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -22,6 +22,7 @@
from typing import Iterator, Optional, Set, TYPE_CHECKING, Union, Dict
import collections.abc
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import QUrl, Qt, QEvent, QTimer, QRect, QPointF
from qutebrowser.qt.gui import QMouseEvent
@@ -35,6 +36,11 @@ if TYPE_CHECKING:
JsValueType = Union[int, float, str, None]
+if machinery.IS_QT6:
+ KeybordModifierType = Qt.KeyboardModifier
+else:
+ KeybordModifierType = Union[Qt.KeyboardModifiers, Qt.KeyboardModifier]
+
class Error(Exception):
@@ -345,7 +351,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
log.webelem.debug("Sending fake click to {!r} at position {} with "
"target {}".format(self, pos, click_target))
- target_modifiers: Dict[usertypes.ClickTarget, Qt.KeyboardModifier] = {
+ target_modifiers: Dict[usertypes.ClickTarget, KeybordModifierType] = {
usertypes.ClickTarget.normal: Qt.KeyboardModifier.NoModifier,
usertypes.ClickTarget.window: Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier,
usertypes.ClickTarget.tab: Qt.KeyboardModifier.ControlModifier,
diff --git a/qutebrowser/browser/webengine/certificateerror.py b/qutebrowser/browser/webengine/certificateerror.py
index 7ee69640f..5484d921d 100644
--- a/qutebrowser/browser/webengine/certificateerror.py
+++ b/qutebrowser/browser/webengine/certificateerror.py
@@ -32,23 +32,36 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
"""A wrapper over a QWebEngineCertificateError.
- Base code shared between Qt 5 and 6 implementations.
+ Support both Qt 5 and 6.
"""
def __init__(self, error: QWebEngineCertificateError) -> None:
super().__init__()
self._error = error
self.ignore = False
- self._validate()
-
- def _validate(self) -> None:
- raise NotImplementedError
def __str__(self) -> str:
- raise NotImplementedError
+ if machinery.IS_QT5:
+ return self._error.errorDescription()
+ else:
+ return self._error.description()
def _type(self) -> Any: # QWebEngineCertificateError.Type or .Error
- raise NotImplementedError
+ if machinery.IS_QT5:
+ return self._error.error()
+ else:
+ return self._error.type()
+
+ def reject_certificate(self) -> None:
+ super().reject_certificate()
+ self._error.rejectCertificate()
+
+ def accept_certificate(self) -> None:
+ super().accept_certificate()
+ if machinery.IS_QT5:
+ self._error.ignoreCertificateError()
+ else:
+ self._error.acceptCertificate()
def __repr__(self) -> str:
return utils.get_repr(
@@ -68,54 +81,6 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
raise usertypes.UndeferrableError("PyQt bug")
-class CertificateErrorWrapperQt5(CertificateErrorWrapper):
-
- """QWebEngineCertificateError handling for Qt 5 API."""
-
- def _validate(self) -> None:
- assert machinery.IS_QT5
-
- def __str__(self) -> str:
- return self._error.errorDescription()
-
- def _type(self) -> Any:
- return self._error.error()
-
- def reject_certificate(self) -> None:
- super().reject_certificate()
- self._error.rejectCertificate()
-
- def accept_certificate(self) -> None:
- super().accept_certificate()
- self._error.ignoreCertificateError()
-
-
-class CertificateErrorWrapperQt6(CertificateErrorWrapper):
-
- """QWebEngineCertificateError handling for Qt 6 API."""
-
- def _validate(self) -> None:
- assert machinery.IS_QT6
-
- def __str__(self) -> str:
- return self._error.description()
-
- def _type(self) -> Any:
- return self._error.type()
-
- def reject_certificate(self) -> None:
- super().reject_certificate()
- self._error.rejectCertificate()
-
- def accept_certificate(self) -> None:
- super().accept_certificate()
- self._error.acceptCertificate()
-
-
def create(error: QWebEngineCertificateError) -> CertificateErrorWrapper:
"""Factory function picking the right class based on Qt version."""
- if machinery.IS_QT5:
- return CertificateErrorWrapperQt5(error)
- elif machinery.IS_QT6:
- return CertificateErrorWrapperQt6(error)
- raise utils.Unreachable
+ return CertificateErrorWrapper(error)
diff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py
index 276f84a62..50f842b10 100644
--- a/qutebrowser/browser/webengine/darkmode.py
+++ b/qutebrowser/browser/webengine/darkmode.py
@@ -296,7 +296,7 @@ _DEFINITIONS[Variant.qt_63] = _DEFINITIONS[Variant.qt_515_3].copy_add_setting(
)
-_PREFERRED_COLOR_SCHEME_DEFINITIONS = {
+_PREFERRED_COLOR_SCHEME_DEFINITIONS: Mapping[Variant, Mapping[Any, str]] = {
Variant.qt_515_2: {
# 0: no-preference (not exposed)
"dark": "1",
diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py
index 7e7e53497..d7f797274 100644
--- a/qutebrowser/browser/webengine/notification.py
+++ b/qutebrowser/browser/webengine/notification.py
@@ -50,6 +50,7 @@ import functools
import subprocess
from typing import Any, List, Dict, Optional, Iterator, Type, TYPE_CHECKING
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import (Qt, QObject, QVariant, QMetaType, QByteArray, pyqtSlot,
pyqtSignal, QTimer, QProcess, QUrl)
from qutebrowser.qt.gui import QImage, QIcon, QPixmap
@@ -686,10 +687,9 @@ def _as_uint32(x: int) -> QVariant:
"""Convert the given int to an uint32 for DBus."""
variant = QVariant(x)
- try:
- # Qt 5
+ if machinery.IS_QT5:
target_type = QVariant.Type.UInt
- except AttributeError:
+ else:
# Qt 6
target_type = QMetaType(QMetaType.Type.UInt.value)
diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py
index 363feeb57..ffacfe8dd 100644
--- a/qutebrowser/browser/webengine/webenginedownloads.py
+++ b/qutebrowser/browser/webengine/webenginedownloads.py
@@ -23,6 +23,7 @@ import re
import os.path
import functools
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import pyqtSlot, Qt, QUrl, QObject
from qutebrowser.qt.webenginecore import QWebEngineDownloadRequest
@@ -44,10 +45,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
parent: QObject = None) -> None:
super().__init__(manager=manager, parent=manager)
self._qt_item = qt_item
- try:
- # Qt 5
+ if machinery.IS_QT5:
qt_item.downloadProgress.connect(self.stats.on_download_progress)
- except AttributeError:
+ else:
# Qt 6
qt_item.receivedBytesChanged.connect(
lambda: self.stats.on_download_progress(
@@ -106,10 +106,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
"{}".format(state_name))
def _do_die(self):
- try:
- # Qt 5
+ if machinery.IS_QT5:
self._qt_item.downloadProgress.disconnect()
- except AttributeError:
+ else:
# Qt 6
self._qt_item.receivedBytesChanged.disconnect()
self._qt_item.totalBytesChanged.disconnect()
diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py
index 3ba72a7d1..dc131bd01 100644
--- a/qutebrowser/browser/webengine/webengineinspector.py
+++ b/qutebrowser/browser/webengine/webengineinspector.py
@@ -19,6 +19,9 @@
"""Customized QWebInspector for QtWebEngine."""
+from typing import Optional
+
+from qutebrowser.qt import machinery
from qutebrowser.qt.webenginewidgets import QWebEngineView
from qutebrowser.qt.webenginecore import QWebEnginePage
from qutebrowser.qt.widgets import QWidget
@@ -48,12 +51,11 @@ class WebEngineInspectorView(QWebEngineView):
See WebEngineView.createWindow for details.
"""
inspected_page = self.page().inspectedPage()
- try:
- # Qt 5
+ if machinery.IS_QT5:
view = inspected_page.view()
assert isinstance(view, QWebEngineView), view
return view.createWindow(wintype)
- except AttributeError:
+ else:
# Qt 6
newpage = inspected_page.createWindow(wintype)
return webview.WebEngineView.forPage(newpage)
@@ -70,7 +72,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
parent: QWidget = None) -> None:
super().__init__(splitter, win_id, parent)
self._check_devtools_resources()
- self._settings = None
+ self._settings: Optional[webenginesettings.WebEngineSettings] = None
def _on_window_close_requested(self) -> None:
"""Called when the 'x' was clicked in the devtools."""
@@ -115,6 +117,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
assert inspector_page.profile() == page.profile()
inspector_page.setInspectedPage(page)
+ assert self._settings is not None
self._settings.update_for_url(inspector_page.requestedUrl())
def _needs_recreate(self) -> bool:
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 37750e343..ef4126047 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -40,7 +40,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
resources, message, jinja, debug, version)
-from qutebrowser.qt import sip
+from qutebrowser.qt import sip, machinery
from qutebrowser.misc import objects, miscwidgets
@@ -85,12 +85,8 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
"""Called from WebEngineTab.connect_signals."""
page = self._widget.page()
page.pdfPrintingFinished.connect(self.pdf_printing_finished)
- try:
- # Qt 6
+ if machinery.IS_QT6:
self._widget.printFinished.connect(self.printing_finished)
- except AttributeError:
- # Qt 5: Uses callbacks instead
- pass
def check_pdf_support(self):
pass
@@ -103,10 +99,9 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
self._widget.page().printToPdf(filename)
def to_printer(self, printer):
- try:
- # Qt 5
+ if machinery.IS_QT5:
self._widget.page().print(printer, self.printing_finished.emit)
- except AttributeError:
+ else:
# Qt 6
self._widget.print(printer)
@@ -1068,8 +1063,7 @@ class _WebEngineScripts(QObject):
def _remove_js(self, name):
"""Remove an early QWebEngineScript."""
scripts = self._widget.page().scripts()
- if hasattr(scripts, 'find'):
- # Qt 6
+ if machinery.IS_QT6:
for script in scripts.find(f'_qute_{name}'):
scripts.remove(script)
else:
@@ -1702,6 +1696,7 @@ class WebEngineTab(browsertab.AbstractTab):
# pylint: disable=protected-access
self.audio._connect_signals()
self.search.connect_signals()
+ assert isinstance(self.printing, WebEnginePrinting)
self.printing.connect_signals()
self._permissions.connect_signals()
self._scripts.connect_signals()
diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py
index 61dfafb30..fdce9f8e9 100644
--- a/qutebrowser/browser/webengine/webview.py
+++ b/qutebrowser/browser/webengine/webview.py
@@ -195,11 +195,10 @@ class WebEnginePage(QWebEnginePage):
self._theme_color = theme_color
self._set_bg_color()
config.instance.changed.connect(self._set_bg_color)
- try:
- self.certificateError.connect(self._handle_certificate_error)
- except AttributeError:
- # Qt 5: Overridden method instead of signal
- pass
+ if machinery.IS_QT6:
+ self.certificateError.connect( # pylint: disable=no-member
+ self._handle_certificate_error
+ )
@config.change_filter('colors.webpage.bg')
def _set_bg_color(self):
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
index f91936257..7170e77dc 100644
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -33,14 +33,17 @@ handle what we actually think we do.
import itertools
import dataclasses
-from typing import Iterator, List, Mapping, Optional, Union, overload
+from typing import Iterator, List, Mapping, Optional, Union, overload, cast
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import Qt, QEvent
from qutebrowser.qt.gui import QKeySequence, QKeyEvent
-try:
- from qutebrowser.qt.core import QKeyCombination
-except ImportError:
- QKeyCombination = None # Qt 6 only
+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
+else:
+ QKeyCombination = None
from qutebrowser.utils import utils, qtutils, debug
@@ -69,9 +72,13 @@ try:
except ValueError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html
- _NIL_KEY = 0
+ _NIL_KEY = 0 # type: ignore[assignment]
_ModifierType = Qt.KeyboardModifier
+if machinery.IS_QT6:
+ _KeyInfoType = QKeyCombination
+else:
+ _KeyInfoType = int
_SPECIAL_NAMES = {
@@ -252,7 +259,7 @@ def _modifiers_to_string(modifiers: _ModifierType) -> str:
_assert_plain_modifier(modifiers)
altgr = Qt.KeyboardModifier.GroupSwitchModifier
if modifiers & altgr:
- modifiers &= ~altgr
+ modifiers &= ~altgr # type: ignore[assignment]
result = 'AltGr+'
else:
result = ''
@@ -376,13 +383,14 @@ class KeyInfo:
except ValueError as ex:
raise InvalidKeyError(str(ex))
key = _remap_unicode(key, e.text())
- modifiers = e.modifiers()
+ modifiers = cast(Qt.KeyboardModifier, e.modifiers())
return cls(key, modifiers)
@classmethod
- def from_qt(cls, combination: Union[int, QKeyCombination]) -> 'KeyInfo':
+ def from_qt(cls, combination: _KeyInfoType) -> 'KeyInfo':
"""Construct a KeyInfo from a Qt5-style int or Qt6-style QKeyCombination."""
- if isinstance(combination, int):
+ if machinery.IS_QT5:
+ assert isinstance(combination, int)
key = Qt.Key(
int(combination) & ~Qt.KeyboardModifier.KeyboardModifierMask)
modifiers = Qt.KeyboardModifier(
@@ -411,7 +419,7 @@ class KeyInfo:
if self.key in _MODIFIER_MAP:
# Don't return e.g. <Shift+Shift>
- modifiers &= ~_MODIFIER_MAP[self.key]
+ modifiers &= ~_MODIFIER_MAP[self.key] # type: ignore[assignment]
elif _is_printable(self.key):
# "normal" binding
if not key_string: # pragma: no cover
@@ -461,25 +469,25 @@ class KeyInfo:
"""Get a QKeyEvent from this KeyInfo."""
return QKeyEvent(typ, self.key, self.modifiers, self.text())
- def to_qt(self) -> Union[int, QKeyCombination]:
+ def to_qt(self) -> _KeyInfoType:
"""Get something suitable for a QKeySequence."""
- if QKeyCombination is None:
- # Qt 5
+ 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)
- 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, key)
def with_stripped_modifiers(self, modifiers: Qt.KeyboardModifier) -> "KeyInfo":
- return KeyInfo(key=self.key, modifiers=self.modifiers & ~modifiers)
+ mods = self.modifiers & ~modifiers
+ return KeyInfo(key=self.key, modifiers=mods) # type: ignore[arg-type]
def is_special(self) -> bool:
"""Check whether this key requires special key syntax."""
@@ -541,7 +549,10 @@ class KeySequence:
def __iter__(self) -> Iterator[KeyInfo]:
"""Iterate over KeyInfo objects."""
- for combination in itertools.chain.from_iterable(self._sequences):
+ combination: QKeySequence
+ for combination in itertools.chain.from_iterable(
+ self._sequences # type: ignore[arg-type]
+ ):
yield KeyInfo.from_qt(combination)
def __repr__(self) -> str:
@@ -648,7 +659,7 @@ class KeySequence:
raise KeyParseError(None, f"Got invalid key: {e}")
_assert_plain_key(key)
- _assert_plain_modifier(ev.modifiers())
+ _assert_plain_modifier(cast(Qt.KeyboardModifier, ev.modifiers()))
key = _remap_unicode(key, ev.text())
modifiers = ev.modifiers()
@@ -675,10 +686,11 @@ class KeySequence:
#
# In addition, Shift also *is* relevant when other modifiers are
# involved. Shift-Ctrl-X should not be equivalent to Ctrl-X.
- if (modifiers == Qt.KeyboardModifier.ShiftModifier and
+ shift_modifier = Qt.KeyboardModifier.ShiftModifier
+ if (modifiers == shift_modifier and # type: ignore[comparison-overlap]
_is_printable(key) and
not ev.text().isupper()):
- modifiers = Qt.KeyboardModifier.NoModifier
+ modifiers = Qt.KeyboardModifier.NoModifier # type: ignore[assignment]
# On macOS, swap Ctrl and Meta back
#
@@ -697,7 +709,7 @@ class KeySequence:
modifiers |= Qt.KeyboardModifier.ControlModifier
infos = list(self)
- infos.append(KeyInfo(key, modifiers))
+ infos.append(KeyInfo(key, cast(Qt.KeyboardModifier, modifiers)))
return self.__class__(*infos)
@@ -712,7 +724,7 @@ class KeySequence:
mappings: Mapping['KeySequence', 'KeySequence']
) -> 'KeySequence':
"""Get a new KeySequence with the given mappings applied."""
- infos = []
+ infos: List[KeyInfo] = []
for info in self:
key_seq = KeySequence(info)
if key_seq in mappings:
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index e4cf8ced1..769f03794 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -567,7 +567,8 @@ class MainWindow(QWidget):
window_flags = Qt.WindowType.Window
refresh_window = self.isVisible()
if hidden:
- window_flags |= Qt.WindowType.CustomizeWindowHint | Qt.WindowType.NoDropShadowWindowHint
+ modifiers = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.NoDropShadowWindowHint
+ window_flags |= modifiers # type: ignore[assignment]
self.setWindowFlags(window_flags)
if utils.is_mac and hidden and not qtutils.version_check('6.3', compiled=False):
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index 2d2990e88..279721b2a 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -197,7 +197,7 @@ class PromptQueue(QObject):
question.completed.connect(loop.deleteLater)
log.prompt.debug("Starting loop.exec() for {}".format(question))
flags = QEventLoop.ProcessEventsFlag.ExcludeSocketNotifiers
- loop.exec(flags)
+ loop.exec(flags) # type: ignore[arg-type]
log.prompt.debug("Ending loop.exec() for {}".format(question))
log.prompt.debug("Restoring old question {}".format(old_question))
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 487f3ea42..bcd07f088 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -223,7 +223,7 @@ class TabbedBrowser(QWidget):
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
self.widget.new_tab_requested.connect(self.tabopen)
self.widget.currentChanged.connect(self._on_current_changed)
- self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
+ self.cur_fullscreen_requested.connect(self.widget.tab_bar().maybe_hide)
self.widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py
index 2603ce23e..28a97fd77 100644
--- a/qutebrowser/misc/sql.py
+++ b/qutebrowser/misc/sql.py
@@ -24,12 +24,12 @@ import collections
import contextlib
import dataclasses
import types
-from typing import Any, Dict, Iterator, List, Mapping, MutableSequence, Optional, Type
+from typing import Any, Dict, Iterator, List, Mapping, MutableSequence, Optional, Type, Union
from qutebrowser.qt.core import QObject, pyqtSignal
from qutebrowser.qt.sql import QSqlDatabase, QSqlError, QSqlQuery
-from qutebrowser.qt import sip
+from qutebrowser.qt import sip, machinery
from qutebrowser.utils import debug, log
@@ -149,6 +149,7 @@ class BugError(Error):
def raise_sqlite_error(msg: str, error: QSqlError) -> None:
"""Raise either a BugError or KnownError."""
error_code = error.nativeErrorCode()
+ primary_error_code: Union[SqliteErrorCode, str]
try:
# https://sqlite.org/rescode.html#pve
primary_error_code = SqliteErrorCode(int(error_code) & 0xff)
@@ -351,10 +352,10 @@ class Query:
def _validate_bound_values(self):
"""Make sure all placeholders are bound."""
qt_bound_values = self.query.boundValues()
- try:
+ if machinery.IS_QT5:
# Qt 5: Returns a dict
- values = qt_bound_values.values()
- except AttributeError:
+ values = list(qt_bound_values.values())
+ else:
# Qt 6: Returns a list
values = qt_bound_values
diff --git a/qutebrowser/qt/sip.py b/qutebrowser/qt/sip.py
index f4fe82a63..1cfaf52aa 100644
--- a/qutebrowser/qt/sip.py
+++ b/qutebrowser/qt/sip.py
@@ -7,18 +7,25 @@ from qutebrowser.qt import machinery
# While upstream recommends using PyQt6.sip ever since PyQt6 5.11, some distributions
# still package later versions of PyQt6 with a top-level "sip" rather than "PyQt6.sip".
+VENDORED_SIP=False
if machinery.USE_PYSIDE6:
raise machinery.Unavailable()
elif machinery.USE_PYQT5:
try:
from PyQt5.sip import *
+ VENDORED_SIP=True
except ImportError:
- from sip import *
+ pass
elif machinery.USE_PYQT6:
try:
from PyQt6.sip import *
+ VENDORED_SIP=True
except ImportError:
- from sip import *
+ pass
+
else:
raise machinery.UnknownWrapper()
+
+if not VENDORED_SIP:
+ from sip import * # type: ignore[import] # pylint: disable=import-error
diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py
index ff6c3b9c2..47cbebb35 100644
--- a/qutebrowser/utils/debug.py
+++ b/qutebrowser/utils/debug.py
@@ -104,7 +104,7 @@ _EnumValueType = Union[sip.simplewrapper, int]
def _qenum_key_python(
value: _EnumValueType,
- klass: Type[_EnumValueType] = None,
+ klass: Type[_EnumValueType],
) -> Optional[str]:
"""New-style PyQt6: Try getting value from Python enum."""
if isinstance(value, enum.Enum) and value.name:
@@ -113,6 +113,7 @@ def _qenum_key_python(
# We got an int with klass passed: Try asking Python enum for member
if issubclass(klass, enum.Enum):
try:
+ assert isinstance(value, int)
name = klass(value).name
if name is not None and name != str(value):
return name
@@ -125,7 +126,7 @@ def _qenum_key_python(
def _qenum_key_qt(
base: Type[_EnumValueType],
value: _EnumValueType,
- klass: Type[_EnumValueType] = None,
+ klass: Type[_EnumValueType],
) -> Optional[str]:
# On PyQt5, or PyQt6 with int passed: Try to ask Qt's introspection.
# However, not every Qt enum value has a staticMetaObject
@@ -168,6 +169,7 @@ def qenum_key(
klass = value.__class__
if klass == int:
raise TypeError("Can't guess enum class of an int!")
+ assert klass is not None
name = _qenum_key_python(value=value, klass=klass)
if name is not None:
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index 0bd9c94e8..699dc86d9 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -36,6 +36,7 @@ import contextlib
from typing import (Any, AnyStr, TYPE_CHECKING, BinaryIO, IO, Iterator,
Optional, Union, Tuple, cast)
+from qutebrowser.qt import machinery, sip
from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray,
QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR,
PYQT_VERSION_STR, QObject, QUrl, QLibraryInfo)
@@ -455,6 +456,12 @@ class QtValueError(ValueError):
super().__init__(err)
+if machinery.IS_QT6:
+ _ProcessEventFlagType = QEventLoop.ProcessEventsFlag
+else:
+ _ProcessEventFlagType = QEventLoop.ProcessEventsFlags
+
+
class EventLoop(QEventLoop):
"""A thin wrapper around QEventLoop.
@@ -468,8 +475,9 @@ class EventLoop(QEventLoop):
def exec(
self,
- flags: QEventLoop.ProcessEventsFlag =
- QEventLoop.ProcessEventsFlag.AllEvents
+ flags: _ProcessEventFlagType = (
+ QEventLoop.ProcessEventsFlag.AllEvents # type: ignore[assignment]
+ ),
) -> int:
"""Override exec_ to raise an exception when re-running."""
if self._executing:
@@ -581,8 +589,7 @@ class LibraryPath(enum.Enum):
def library_path(which: LibraryPath) -> pathlib.Path:
"""Wrapper around QLibraryInfo.path / .location."""
- if hasattr(QLibraryInfo, "path"):
- # Qt 6
+ if machinery.IS_QT6:
val = getattr(QLibraryInfo.LibraryPath, which.value)
ret = QLibraryInfo.path(val)
else:
@@ -593,7 +600,7 @@ def library_path(which: LibraryPath) -> pathlib.Path:
return pathlib.Path(ret)
-def extract_enum_val(val: Union[int, enum.Enum]) -> int:
+def extract_enum_val(val: Union[sip.simplewrapper, int, enum.Enum]) -> int:
"""Extract an int value from a Qt enum value.
For Qt 5, enum values are basically Python integers.
@@ -602,4 +609,6 @@ def extract_enum_val(val: Union[int, enum.Enum]) -> int:
"""
if isinstance(val, enum.Enum):
return val.value
+ elif isinstance(val, sip.simplewrapper):
+ return int(val) # type: ignore[call-overload]
return int(val)
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index b84af4524..aadda000b 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -491,7 +491,7 @@ class AbstractCertificateErrorWrapper:
"""A wrapper over an SSL/certificate error."""
def __init__(self) -> None:
- self._certificate_accepted = None
+ self._certificate_accepted: Optional[bool] = None
def __str__(self) -> str:
raise NotImplementedError
@@ -514,7 +514,7 @@ class AbstractCertificateErrorWrapper:
def defer(self) -> None:
raise NotImplementedError
- def certificate_was_accepted(self) -> None:
+ def certificate_was_accepted(self) -> bool:
"""Check whether the certificate was accepted by the user."""
if not self.is_overridable():
return False
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 997668f97..6a318c0ae 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -785,18 +785,19 @@ def qtwebengine_versions(*, avoid_init: bool = False) -> WebEngineVersions:
if override is not None:
return WebEngineVersions.from_pyqt(override, source='override')
- try:
- from qutebrowser.qt.webenginecore import (
- qWebEngineVersion,
- qWebEngineChromiumVersion,
- )
- except ImportError:
- pass # Needs QtWebEngine 6.2+ with PyQtWebEngine 6.3.1+
- else:
- return WebEngineVersions.from_api(
- qtwe_version=qWebEngineVersion(),
- chromium_version=qWebEngineChromiumVersion(),
- )
+ if machinery.IS_QT6:
+ try:
+ from qutebrowser.qt.webenginecore import (
+ qWebEngineVersion,
+ qWebEngineChromiumVersion,
+ )
+ except ImportError:
+ pass # Needs QtWebEngine 6.2+ with PyQtWebEngine 6.3.1+
+ else:
+ return WebEngineVersions.from_api(
+ qtwe_version=qWebEngineVersion(),
+ chromium_version=qWebEngineChromiumVersion(),
+ )
from qutebrowser.browser.webengine import webenginesettings
@@ -1023,13 +1024,11 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
vp.setVersion(2, 0)
try:
- try:
- # Qt 5
+ if machinery.IS_QT5:
vf = ctx.versionFunctions(vp)
- except AttributeError:
+ else:
# Qt 6
# FIXME:qt6 (lint)
- # pylint: disable-next=no-name-in-module
from qutebrowser.qt.opengl import QOpenGLVersionFunctionsFactory
vf = QOpenGLVersionFunctionsFactory.get(vp, ctx)
except ImportError as e:
diff --git a/tox.ini b/tox.ini
index bf7b1fcbd..0a970064f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -180,16 +180,19 @@ deps =
whitelist_externals = bash
commands = bash scripts/dev/run_shellcheck.sh {posargs}
-[testenv:mypy]
+[testenv:mypy-{pyqt5,pyqt6}]
basepython = {env:PYTHON:python3}
passenv = TERM MYPY_FORCE_TERMINAL_WIDTH
+setenv =
+ pyqt6: 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: 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
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-dev.txt
-r{toxinidir}/misc/requirements/requirements-tests.txt
-r{toxinidir}/misc/requirements/requirements-mypy.txt
commands =
- {envpython} -m mypy qutebrowser {posargs}
+ {envpython} -m mypy {env:CONSTANTS_ARGS} qutebrowser {posargs}
[testenv:yamllint]
basepython = {env:PYTHON:python3}
@@ -204,12 +207,15 @@ whitelist_externals = actionlint
commands =
actionlint
-[testenv:mypy-diff]
+[testenv:mypy-{pyqt5,pyqt6}-diff]
basepython = {env:PYTHON:python3}
-passenv = {[testenv:mypy]passenv}
-deps = {[testenv:mypy]deps}
+passenv = {[testenv:mypy-pyqt6]passenv}
+deps = {[testenv:mypy-pyqt6]deps}
+setenv =
+ pyqt6: 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: 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
commands =
- {envpython} -m mypy --cobertura-xml-report {envtmpdir} qutebrowser tests {posargs}
+ {envpython} -m mypy --cobertura-xml-report {envtmpdir} {env: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
[testenv:sphinx]