Merge pull request #7392 from qutebrowser/feat/turn_on_mypy_pyqt5

Get mypy working, with pyqt5, on qt6-v2 branch
This commit is contained in:
toofar 2022-09-16 16:58:49 +12:00 committed by GitHub
commit be5b4c5dd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 181 additions and 168 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

18
tox.ini
View File

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