From c1738ca55006966e7c0ad9eacbfb58625aa88c44 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 10:17:28 +1200 Subject: tox: split mypy env into mypy-{qt5,qt6} Would be nice to have a bare `mypy` env which ran both the more specific ones in sequence but I don't know how to do that. Not sure if there is a way to pull the CONSTANTS_ARGS stuff out to a non-env parameter and pass it into commands but I couldn't figure out a way. So via the environment it is. TODO: compare PyQt6 as-is with the WIP PyQt6-Stub --- .github/workflows/ci.yml | 3 ++- misc/requirements/requirements-mypy.txt | 2 ++ tox.ini | 18 ++++++++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd4a223a9..9fd0fb449 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-pyqt5 + # - testenv: mypy-pyqt6 - 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/tox.ini b/tox.ini index bf7b1fcbd..8f55638a6 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-{qt5,qt6}] basepython = {env:PYTHON:python3} passenv = TERM MYPY_FORCE_TERMINAL_WIDTH +setenv = + qt6: 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 + qt5: 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-{qt5,qt6}-diff] basepython = {env:PYTHON:python3} -passenv = {[testenv:mypy]passenv} -deps = {[testenv:mypy]deps} +passenv = {[testenv:mypy-qt6]passenv} +deps = {[testenv:mypy-qt6]deps} +setenv = + qt6: 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 + qt5: 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] -- cgit v1.2.3-54-g00ecf From 76ca4a9dd61e7179881044757d83d2bf5928819d Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 10:27:11 +1200 Subject: mypy: handle sip conditional imports Putting ignore[type] on the second import works fine. But if we have it on both the pyqt5 and pyqt6 branch it'll complain about the other branch on each run. So pull it out to a common place we can ignore. --- qutebrowser/qt/sip.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qutebrowser/qt/sip.py b/qutebrowser/qt/sip.py index f4fe82a63..6430aa482 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] -- cgit v1.2.3-54-g00ecf From 4647125172bfa10bd1b5624c34bf4400011b6ada Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 10:32:07 +1200 Subject: mypy: fix hints for abstract cert error wrapper --- qutebrowser/utils/usertypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 -- cgit v1.2.3-54-g00ecf From 56822836c5568ce6d5238e3b579770ebb8846b13 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 10:46:58 +1200 Subject: mypy: fix exec() type hints? PyQt5 exposes Enums and their corresponding QtFlags objects seperately, and for some reason they aren't interchangeable. ref https://github.com/python-qt-tools/PyQt5-stubs/issues/142 We could handle this by casting values back and forth between the enum value (for working with) and the flags value (for passing to methods), but this situation doesn't even exist with PyQt6, you can use everything as enums just fine. So I'm just adding ignore comments and making type definitions conditional. Not sure if this is the right thing to be doing or not. for b3cdb28d044 --- qutebrowser/utils/qtutils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 0bd9c94e8..87f74425e 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -455,6 +455,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 +474,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: -- cgit v1.2.3-54-g00ecf From e76a063e63b95fbb850649b09beeb5ccdcc83c60 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 10:52:38 +1200 Subject: mypy: defer to machinery for conditional: QLibraryInfo --- qutebrowser/utils/qtutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 87f74425e..430f15c75 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 from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR, PYQT_VERSION_STR, QObject, QUrl, QLibraryInfo) @@ -588,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: -- cgit v1.2.3-54-g00ecf From 42d3cca794703ed600757976ea63cfdf66cf1f07 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 11:05:47 +1200 Subject: mypy: fallback sqlite error code can be str --- qutebrowser/misc/sql.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 2603ce23e..78692fa6c 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -24,7 +24,7 @@ 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 @@ -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) -- cgit v1.2.3-54-g00ecf From 9e1dfacef73279d6a950d4b0c474367c275d3f42 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 11:06:47 +1200 Subject: mypy: defer to machinery for conditional: QtSql bound values Also turn the qt5 value into a list instead of a dict_values, for the sake of mypy --- qutebrowser/misc/sql.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 78692fa6c..28a97fd77 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -29,7 +29,7 @@ from typing import Any, Dict, Iterator, List, Mapping, MutableSequence, Optional 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 @@ -352,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 -- cgit v1.2.3-54-g00ecf From 647b74197a779e00cae1847654796581df28a7b0 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 11:32:00 +1200 Subject: mypy: move conditionals for CertificateErrorWrapper into class Get rid of the per-backend classes and move the backend specific conditionals into a common class. Sadly it seems mypy isn't clever enough to reason ignore a class it should know is never used. Possibly it looks at them at parse time. Probably putting the whole class definitions into conditionals would do it but I'm not sure if I want to go down that route for such a small example. Hopefully there aren't too many more of these. --- qutebrowser/browser/webengine/certificateerror.py | 77 +++++++---------------- 1 file changed, 21 insertions(+), 56 deletions(-) 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) -- cgit v1.2.3-54-g00ecf From fda08527f1d4d02666cc8d76040286fb69c1ac82 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 11:34:41 +1200 Subject: mypy: defer to machinery for conditional: QVariant --- qutebrowser/browser/webengine/notification.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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) -- cgit v1.2.3-54-g00ecf From 04f1ae74bd6846b9cb376d94d180ff4fdd04698c Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 11:45:35 +1200 Subject: mypy: defer to machinery for conditional: qWebEngineVersion --- qutebrowser/utils/version.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 997668f97..24e00cfeb 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 -- cgit v1.2.3-54-g00ecf From 5293009413b3eb2ba648876455bcea2755dd5a1b Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 11:45:52 +1200 Subject: mypy: defer to machinery for conditional: QOpenGLVersionProfile --- qutebrowser/utils/version.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 24e00cfeb..7ff13e260 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -1024,10 +1024,9 @@ 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 -- cgit v1.2.3-54-g00ecf From a70954a65f6ed86a5fa0146078ed4dfff1cffc00 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 11:50:25 +1200 Subject: mypy: fix KeyboardModifier type in webelem? Conditionally define a type so it can work with PyQt5s mismatch of enums and flags and PyQt6s enums. --- qutebrowser/browser/webelem.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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, -- cgit v1.2.3-54-g00ecf From db8cb25bd3bec028b2cc4b2ffa74f408e6bfcd89 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 11:58:12 +1200 Subject: mypy: ignore enum binary operator: UrlFormattingOption Yet another case of confusion between flags and enum objects. --- qutebrowser/browser/hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- cgit v1.2.3-54-g00ecf From 5be5b6bb41684cd7620f7168cfe212d0bf6328db Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:02:14 +1200 Subject: mypy: defer to machinery for conditional: QDownloadItem --- qutebrowser/browser/webengine/webenginedownloads.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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() -- cgit v1.2.3-54-g00ecf From 3fae7367777406b15da84e33263a1115363260d1 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:04:44 +1200 Subject: mypy: fix exec() type hints?: prompt PyQt5 stubs require a flags object that doesn't exist in PyQt6 --- qutebrowser/mainwindow/prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) -- cgit v1.2.3-54-g00ecf From 77a90cab1e2ccf4d6b23107304cffed6575a98aa Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:09:57 +1200 Subject: mypy: ignore enum binary operator: Qt.WindowType They are the same damn thing: https://doc.qt.io/qt-6/qt.html#WindowType-enum --- qutebrowser/mainwindow/mainwindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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): -- cgit v1.2.3-54-g00ecf From 1bbf75ae7dbbfa2568bd5fe50dc1d7d213377f4e Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:18:54 +1200 Subject: mypy: add Optional hint to QPrintDialog --- qutebrowser/browser/browsertab.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 811a530a7..29eb06e55 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 -- cgit v1.2.3-54-g00ecf From b64fdfc5842f9be9637887aff38b313a600e7bed Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:26:39 +1200 Subject: mypy: ignore enum argument type: UrlFormattingOption The docs say: > The options from QUrl::ComponentFormattingOptions are also possible. > The FormattingOptions type is a typedef for QFlags. It stores an OR combination of UrlFormattingOption values. Maybe we should be should be definining out own types for some of the enums that include both the QFlag, enum and any child enums. --- qutebrowser/browser/browsertab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 29eb06e55..9c6c52e0e 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -1322,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: -- cgit v1.2.3-54-g00ecf From 7a2c41c785fce0d8051c0cf8c4f7ad23646d63fa Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:34:00 +1200 Subject: mypy: access our tab_bar implementation, not tabBar Apparently this got changed by the scripts/dev/rewrite_enums.py script? Strange. 0877fb0d78635 --- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) -- cgit v1.2.3-54-g00ecf From aef684cbffd6da7b75cf9dae3deb7997bebf1035 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:36:59 +1200 Subject: mypy: defer to machinery for conditional: webview.certError --- qutebrowser/browser/webengine/webview.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 61dfafb30..bb0e3856a 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -195,11 +195,8 @@ class WebEnginePage(QWebEnginePage): self._theme_color = theme_color self._set_bg_color() config.instance.changed.connect(self._set_bg_color) - try: + if machinery.IS_QT6: self.certificateError.connect(self._handle_certificate_error) - except AttributeError: - # Qt 5: Overridden method instead of signal - pass @config.change_filter('colors.webpage.bg') def _set_bg_color(self): -- cgit v1.2.3-54-g00ecf From 802dc459dca2b4f13f7d855310a266a3f13bad22 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:44:16 +1200 Subject: mypy: defer to machinery for conditional: inspector API --- qutebrowser/browser/webengine/webengineinspector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index 3ba72a7d1..68093a090 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -19,6 +19,7 @@ """Customized QWebInspector for QtWebEngine.""" +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 +49,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) -- cgit v1.2.3-54-g00ecf From 52c3caaa05929eb389c4212be020a113f1aa3280 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:44:55 +1200 Subject: mypy: Add Optional hint to inspector._settings --- qutebrowser/browser/webengine/webengineinspector.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index 68093a090..dc131bd01 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -19,6 +19,8 @@ """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 @@ -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: -- cgit v1.2.3-54-g00ecf From 3e9fd4ed7eb34f250736bdfe3ff39aded4869e69 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 12:49:33 +1200 Subject: mypy: defer to machinery for conditional: printing --- qutebrowser/browser/webengine/webenginetab.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 37750e343..83f6b9b75 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) -- cgit v1.2.3-54-g00ecf From 046244b54ddb1e95b63da78789137b7efe7b489e Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 13:07:13 +1200 Subject: mypy: defer to machinery for conditional: QWebEngineScripts --- qutebrowser/browser/webengine/webenginetab.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 83f6b9b75..d22651fbb 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1063,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: -- cgit v1.2.3-54-g00ecf From 9c193c18f08f30c341e4869d5e84a4564a58afd1 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 13:07:39 +1200 Subject: mypy: cast to subtype: WebEnginePrinting --- qutebrowser/browser/webengine/webenginetab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index d22651fbb..ef4126047 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1696,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() -- cgit v1.2.3-54-g00ecf From b0e3dcef819d9e24361051d2dc85f6f0e8762aa6 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 13:13:46 +1200 Subject: mypy: add type hint for defs in darkmode.py Not sure why mypy was failing to see the inner dicts in _PREFERRED_COLOR_SCHEME_DEFINITIONS where being seen as "object" by mypy and not dict, I think the syntax is correct. Add some basic type hints to help it. They Any is because usertype.UNSET() is a sentinal object --- qutebrowser/browser/webengine/darkmode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", -- cgit v1.2.3-54-g00ecf From baeb05d6a2403a83c134d6465eac922ce8c87cde Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 13:39:53 +1200 Subject: mypy: handle none case in Optional: downloads.reply I'm not sure how theoretical this case is. There is a comment somewhere else indicating the reply can "disappear". --- qutebrowser/browser/qtnetworkdownloads.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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() -- cgit v1.2.3-54-g00ecf From ed13c3c734d265fc92b9b4ea118505aed0aa4414 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 14:00:10 +1200 Subject: mypy: ignores for another enum: KeyboardModifier There are several cases where PyQt5 methods expects the plural flags version but we've got the singular Enum version from accessing enum members directly. It's not easy to turn those enums into flags and the flags don't even exist in PyQt6. --- qutebrowser/keyinput/keyutils.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index f91936257..3af103607 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -33,7 +33,7 @@ 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.core import Qt, QEvent from qutebrowser.qt.gui import QKeySequence, QKeyEvent @@ -69,7 +69,7 @@ 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 @@ -252,7 +252,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,7 +376,7 @@ 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 @@ -411,7 +411,7 @@ class KeyInfo: if self.key in _MODIFIER_MAP: # Don't return e.g. - 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 @@ -479,7 +479,8 @@ class KeyInfo: 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.""" @@ -648,7 +649,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 +676,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 +699,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) -- cgit v1.2.3-54-g00ecf From 2eacf0456a7b5e7c7cb519e59c191dc257752214 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 14:04:37 +1200 Subject: mypy: defer to machinery for conditional: QKeyCombination That wasn't so hard, just put everything behind compile time constants, even the type definition. --- qutebrowser/keyinput/keyutils.py | 41 ++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 3af103607..92ef717af 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -35,12 +35,13 @@ import itertools import dataclasses 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: +if machinery.IS_QT6: from qutebrowser.qt.core import QKeyCombination -except ImportError: - QKeyCombination = None # Qt 6 only +else: + QKeyCombination = None from qutebrowser.utils import utils, qtutils, debug @@ -72,6 +73,10 @@ except ValueError: _NIL_KEY = 0 # type: ignore[assignment] _ModifierType = Qt.KeyboardModifier +if machinery.IS_QT6: + _KeyInfoType = QKeyCombination +else: + _KeyInfoType = int _SPECIAL_NAMES = { @@ -380,9 +385,10 @@ class KeyInfo: 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( @@ -461,22 +467,21 @@ 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": mods = self.modifiers & ~modifiers -- cgit v1.2.3-54-g00ecf From 3d93e1537836e91580eae32714f440626a2a1e15 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 14:19:49 +1200 Subject: mypy: add hints and comments around QKeySequence Mypy doesn't seem to know that QKeySequence is an iterable, so it complains about itertools not handling it. And since we lose type information there we have to add it back in a couple of places. --- qutebrowser/keyinput/keyutils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 92ef717af..6016d61f4 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -547,7 +547,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: @@ -719,7 +722,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: -- cgit v1.2.3-54-g00ecf From 377bdf736f96605b998ca6f2ea2b062e190653b0 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 15:05:37 +1200 Subject: mypy: fix qenum debug type hints? 1. stop pretending to propagate None to the qt/python debug methods 2. handle simplewrapper in extract_enum_val I think this stuff will need a little more cleaning up when we get to sorting out type checking on PyQt6. The whole key debug module seems to be a bit fuzzy about when it's going to be passing around a simplewrapper, int and enum. I believe we are using sip.simplerwapper as a common parent type of all the Qt Flag/Enum values. I think it gets better on PyQt6, don't remember though. It might just change to be sip.wrapper instead. --- qutebrowser/utils/debug.py | 6 ++++-- qutebrowser/utils/qtutils.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) 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 430f15c75..699dc86d9 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -36,7 +36,7 @@ import contextlib from typing import (Any, AnyStr, TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, Tuple, cast) -from qutebrowser.qt import machinery +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) @@ -600,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. @@ -609,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) -- cgit v1.2.3-54-g00ecf From 4b3ec40eb0435950d1278f3477d74027e4b287ce Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 15:12:32 +1200 Subject: mypy: re-enable CI jobs for pyqt5 We'll be going forward with type checking on PyQt5 for now while figuring out what to do with PyQt6 type checking. See https://github.com/qutebrowser/qutebrowser/discussions/7372#discussioncomment-3502200 --- .github/workflows/ci.yml | 2 +- tox.ini | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fd0fb449..d38b995c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,8 @@ jobs: - testenv: pylint - testenv: flake8 # FIXME:qt6 (lint) - # - testenv: mypy-pyqt5 # - testenv: mypy-pyqt6 + - testenv: mypy-pyqt5 - testenv: docs - testenv: vulture - testenv: misc diff --git a/tox.ini b/tox.ini index 8f55638a6..0a970064f 100644 --- a/tox.ini +++ b/tox.ini @@ -180,12 +180,12 @@ deps = whitelist_externals = bash commands = bash scripts/dev/run_shellcheck.sh {posargs} -[testenv:mypy-{qt5,qt6}] +[testenv:mypy-{pyqt5,pyqt6}] basepython = {env:PYTHON:python3} passenv = TERM MYPY_FORCE_TERMINAL_WIDTH setenv = - qt6: 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 - qt5: 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 + 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 @@ -207,13 +207,13 @@ whitelist_externals = actionlint commands = actionlint -[testenv:mypy-{qt5,qt6}-diff] +[testenv:mypy-{pyqt5,pyqt6}-diff] basepython = {env:PYTHON:python3} -passenv = {[testenv:mypy-qt6]passenv} -deps = {[testenv:mypy-qt6]deps} +passenv = {[testenv:mypy-pyqt6]passenv} +deps = {[testenv:mypy-pyqt6]deps} setenv = - qt6: 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 - qt5: 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 + 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} {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 -- cgit v1.2.3-54-g00ecf From 71e9b9d854f98627dcec378ed2b0b696177d66ba Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 11 Sep 2022 17:17:46 +1200 Subject: fix lint: qt5 vs qt6 confusion --- qutebrowser/browser/webengine/webview.py | 4 +++- qutebrowser/keyinput/keyutils.py | 4 +++- qutebrowser/qt/sip.py | 2 +- qutebrowser/utils/version.py | 1 - 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index bb0e3856a..fdce9f8e9 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -196,7 +196,9 @@ class WebEnginePage(QWebEnginePage): self._set_bg_color() config.instance.changed.connect(self._set_bg_color) if machinery.IS_QT6: - self.certificateError.connect(self._handle_certificate_error) + 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 6016d61f4..7170e77dc 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -39,7 +39,9 @@ from qutebrowser.qt import machinery from qutebrowser.qt.core import Qt, QEvent from qutebrowser.qt.gui import QKeySequence, QKeyEvent if machinery.IS_QT6: - from qutebrowser.qt.core import QKeyCombination + # 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 diff --git a/qutebrowser/qt/sip.py b/qutebrowser/qt/sip.py index 6430aa482..1cfaf52aa 100644 --- a/qutebrowser/qt/sip.py +++ b/qutebrowser/qt/sip.py @@ -28,4 +28,4 @@ else: raise machinery.UnknownWrapper() if not VENDORED_SIP: - from sip import * # type: ignore[import] + from sip import * # type: ignore[import] # pylint: disable=import-error diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 7ff13e260..6a318c0ae 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -1029,7 +1029,6 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover 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: -- cgit v1.2.3-54-g00ecf