diff options
author | toofar <toofar@spalge.com> | 2022-09-16 16:58:49 +1200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-16 16:58:49 +1200 |
commit | be5b4c5dd0d137cfbd00e57608787350df9eae67 (patch) | |
tree | 939801db4cd3bed3ce8373be383488ff2b2ab49e | |
parent | 8decafefbe94c111c1cd1b59aeb0b314cb4de5c9 (diff) | |
parent | 71e9b9d854f98627dcec378ed2b0b696177d66ba (diff) | |
download | qutebrowser-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
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: @@ -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] |