diff options
66 files changed, 731 insertions, 437 deletions
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index a0dbeab37..b8bce1545 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -169,6 +169,7 @@ Changed `content.site_specific_quirks.skip`, so that `String.replaceAll` is now polyfilled on QtWebEngine < 5.15.3, hopefully improving website compaitibility. +- Hints are now displayed for elements setting an `aria-haspopup` attribute. Fixed ~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 1b28eb39f..7767f1eea 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -3510,6 +3510,7 @@ Default: * +pass:[[role="menuitemcheckbox"\]]+ * +pass:[[role="menuitemradio"\]]+ * +pass:[[role="treeitem"\]]+ +* +pass:[[aria-haspopup\]]+ * +pass:[[ng-click\]]+ * +pass:[[ngClick\]]+ * +pass:[[data-ng-click\]]+ diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index cc0fb59b0..01815b647 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -5,15 +5,15 @@ build==0.10.0 bump2version==1.0.1 certifi==2023.5.7 cffi==1.15.1 -charset-normalizer==3.1.0 -cryptography==41.0.1 +charset-normalizer==3.2.0 +cryptography==41.0.2 docutils==0.20.1 github3.py==4.0.1 hunter==3.6.1 idna==3.4 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 -jaraco.classes==3.2.3 +importlib-metadata==6.8.0 +importlib-resources==6.0.0 +jaraco.classes==3.3.0 jeepney==0.8.0 keyring==24.2.0 manhole==1.8.0 @@ -25,7 +25,7 @@ pkginfo==1.9.6 ply==3.11 pycparser==2.21 Pygments==2.15.1 -PyJWT==2.7.0 +PyJWT==2.8.0 Pympler==1.0.1 pyproject_hooks==1.0.0 PyQt-builder==1.15.1 @@ -40,8 +40,8 @@ sip==6.7.9 six==1.16.0 tomli==2.0.1 twine==4.0.2 -typing_extensions==4.6.3 +typing_extensions==4.7.1 uritemplate==4.1.1 -# urllib3==2.0.3 +# urllib3==2.0.4 webencodings==0.5.1 -zipp==3.15.0 +zipp==3.16.2 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 018c13a3c..685542224 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -2,17 +2,17 @@ attrs==23.1.0 flake8==6.0.0 -flake8-bugbear==23.6.5 +flake8-bugbear==23.7.10 flake8-builtins==2.1.0 -flake8-comprehensions==3.13.0 +flake8-comprehensions==3.14.0 flake8-debugger==4.1.2 flake8-deprecated==2.0.1 flake8-docstrings==1.7.0 flake8-future-import==0.4.7 -flake8-plugin-utils==1.3.2 +flake8-plugin-utils==1.3.3 flake8-pytest-style==1.7.2 flake8-string-format==0.3.0 -flake8-tidy-imports==4.9.0 +flake8-tidy-imports==4.10.0 flake8-tuple==0.4.1 mccabe==0.7.0 pep8-naming==0.13.3 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index b5d23bf61..24feda7d6 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,11 +1,10 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py chardet==5.1.0 -diff-cover==7.6.0 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 +diff-cover==7.7.0 +importlib-resources==6.0.0 Jinja2==3.1.2 -lxml==4.9.2 +lxml==4.9.3 MarkupSafe==2.1.3 mypy==1.4.1 mypy-extensions==1.0.0 @@ -13,11 +12,10 @@ pluggy==1.2.0 Pygments==2.15.1 PyQt5-stubs==5.15.6.0 tomli==2.0.1 -PyQt6-stubs @ git+https://github.com/python-qt-tools/PyQt6-stubs.git@f623a641cd5cdff53342177e4fbbf9cae8172336 types-colorama==0.4.15.11 types-docutils==0.20.0.1 types-Pygments==2.15.0.1 types-PyYAML==6.0.12.10 -typing_extensions==4.6.3 -zipp==3.15.0 -types-setuptools==68.0.0.0 +types-setuptools==68.0.0.2 +typing_extensions==4.7.1 +zipp==3.16.2 diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw index 487d30ca6..027f4fef6 100644 --- a/misc/requirements/requirements-mypy.txt-raw +++ b/misc/requirements/requirements-mypy.txt-raw @@ -3,7 +3,6 @@ lxml # For HTML reports diff-cover PyQt5-stubs -git+https://github.com/python-qt-tools/PyQt6-stubs.git types-PyYAML types-colorama types-Pygments diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 1652cd225..759c6f11f 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -2,4 +2,4 @@ altgraph==0.17.3 pyinstaller==5.13.0 -pyinstaller-hooks-contrib==2023.3 +pyinstaller-hooks-contrib==2023.5 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index fd5e29978..e7a24df51 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,10 +1,10 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==2.15.5 +astroid==2.15.6 certifi==2023.5.7 cffi==1.15.1 -charset-normalizer==3.1.0 -cryptography==41.0.1 +charset-normalizer==3.2.0 +cryptography==41.0.2 dill==0.3.6 github3.py==4.0.1 idna==3.4 @@ -12,15 +12,17 @@ isort==5.12.0 lazy-object-proxy==1.9.0 mccabe==0.7.0 pefile==2023.2.7 -platformdirs==3.8.0 +platformdirs==3.9.1 pycparser==2.21 -PyJWT==2.7.0 +PyJWT==2.8.0 pylint==2.17.4 python-dateutil==2.8.2 ./scripts/dev/pylint_checkers requests==2.31.0 six==1.16.0 +tomli==2.0.1 tomlkit==0.11.8 +typing_extensions==4.7.1 uritemplate==4.1.1 -# urllib3==2.0.3 +# urllib3==2.0.4 wrapt==1.15.0 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index e0d7fe585..6aa40fd97 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -2,7 +2,7 @@ build==0.10.0 certifi==2023.5.7 -charset-normalizer==3.1.0 +charset-normalizer==3.2.0 docutils==0.20.1 idna==3.4 packaging==23.1 @@ -11,5 +11,5 @@ pyproject_hooks==1.0.0 pyroma==4.2 requests==2.31.0 tomli==2.0.1 -trove-classifiers==2023.5.24 -urllib3==2.0.3 +trove-classifiers==2023.7.6 +urllib3==2.0.4 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index e92e41b8c..a9cafa9d3 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -3,11 +3,11 @@ alabaster==0.7.13 Babel==2.12.1 certifi==2023.5.7 -charset-normalizer==3.1.0 +charset-normalizer==3.2.0 docutils==0.20.1 idna==3.4 imagesize==1.4.1 -importlib-metadata==6.7.0 +importlib-metadata==6.8.0 Jinja2==3.1.2 MarkupSafe==2.1.3 packaging==23.1 @@ -22,5 +22,5 @@ sphinxcontrib-htmlhelp==2.0.1 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 -urllib3==2.0.3 -zipp==3.15.0 +urllib3==2.0.4 +zipp==3.16.2 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index abc44017a..137d01ca5 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -4,16 +4,18 @@ attrs==23.1.0 beautifulsoup4==4.12.2 blinker==1.6.2 certifi==2023.5.7 -charset-normalizer==3.1.0 +charset-normalizer==3.2.0 cheroot==10.0.0 -click==8.1.3 +click==8.1.6 coverage==7.2.7 -execnet==1.9.0 +exceptiongroup==1.1.2 +execnet==2.0.2 filelock==3.12.2 Flask==2.3.2 hunter==3.6.1 -hypothesis==6.79.3 +hypothesis==6.82.0 idna==3.4 +importlib-metadata==6.8.0 iniconfig==2.0.0 itsdangerous==2.1.2 jaraco.functools==3.8.0 @@ -24,7 +26,7 @@ manhole==1.8.0 more-itertools==9.1.0 packaging==23.1 parse==1.19.1 -parse-type==0.6.0 +parse-type==0.6.2 pluggy==1.2.0 py-cpuinfo==9.0.0 Pygments==2.15.1 @@ -36,7 +38,7 @@ pytest-instafail==0.5.0 pytest-mock==3.11.1 pytest-qt==4.2.0 pytest-repeat==0.9.1 -pytest-rerunfailures==11.1.2 +pytest-rerunfailures==12.0 pytest-xdist==3.3.1 pytest-xvfb==3.0.0 PyVirtualDisplay==3.0 @@ -47,7 +49,9 @@ sortedcontainers==2.4.0 soupsieve==2.4.1 tldextract==3.4.4 toml==0.10.2 -typing_extensions==4.6.3 -urllib3==2.0.3 +tomli==2.0.1 +typing_extensions==4.7.1 +urllib3==2.0.4 vulture==2.7 Werkzeug==2.3.6 +zipp==3.16.2 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index e68e79d46..a522764bd 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -3,15 +3,15 @@ cachetools==5.3.1 chardet==5.1.0 colorama==0.4.6 -distlib==0.3.6 +distlib==0.3.7 filelock==3.12.2 packaging==23.1 -pip==23.1.2 -platformdirs==3.8.0 +pip==23.2 +platformdirs==3.9.1 pluggy==1.2.0 -pyproject-api==1.5.2 +pyproject-api==1.5.3 setuptools==68.0.0 tomli==2.0.1 -tox==4.6.3 -virtualenv==20.23.1 +tox==4.6.4 +virtualenv==20.24.1 wheel==0.40.0 diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt index 718012a4e..a35c0ff58 100644 --- a/misc/requirements/requirements-yamllint.txt +++ b/misc/requirements/requirements-yamllint.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py pathspec==0.11.1 -PyYAML==6.0 +PyYAML==6.0.1 yamllint==1.32.0 diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 47cba5922..6f68956f8 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -286,10 +286,16 @@ class AbstractPrinting(QObject): """ raise NotImplementedError + def _do_print(self) -> None: + assert self._dialog is not None + printer = self._dialog.printer() + assert printer is not None + self.to_printer(printer) + def show_dialog(self) -> None: """Print with a QPrintDialog.""" - self._dialog = dialog = QPrintDialog(self._tab) - self._dialog.open(lambda: self.to_printer(dialog.printer())) + self._dialog = QPrintDialog(self._tab) + self._dialog.open(self._do_print) # Gets cleaned up in on_printing_finished diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3b38c44c0..410b844a0 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1597,8 +1597,7 @@ class CommandDispatcher: def _search_navigation_cb(self, result): """Callback called from :search-prev/next.""" if result == browsertab.SearchNavigationResult.not_found: - # FIXME check if this actually can happen... - message.warning("Search result vanished...") + self._search_cb(found=False, text=self._tabbed_browser.search_text) return elif result == browsertab.SearchNavigationResult.found: return diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index f4790bc9f..02bba7a41 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -77,9 +77,10 @@ class DownloadView(QListView): self.clicked.connect(self.on_clicked) def __repr__(self): - model = self.model() + model = qtutils.add_optional(self.model()) + count: Union[int, str] if model is None: - count = 'None' # type: ignore[unreachable] + count = 'None' else: count = model.rowCount() return utils.get_repr(self, count=count) @@ -173,9 +174,12 @@ class DownloadView(QListView): assert name is not None assert handler is not None action = self._menu.addAction(name) + assert action is not None action.triggered.connect(handler) if actions: - self._menu.popup(self.viewport().mapToGlobal(point)) + viewport = self.viewport() + assert viewport is not None + self._menu.popup(viewport.mapToGlobal(point)) def minimumSizeHint(self): """Override minimumSizeHint so the size is correct in a layout.""" diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index c0b23040c..a83621ae0 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -23,8 +23,9 @@ import contextlib import pathlib from typing import cast, Mapping, MutableSequence, Optional +from qutebrowser.qt import machinery from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, pyqtSignal -from qutebrowser.qt.widgets import QProgressDialog, QApplication +from qutebrowser.qt.widgets import QProgressDialog, QApplication, QPushButton from qutebrowser.config import config from qutebrowser.api import cmdutils @@ -54,7 +55,13 @@ class HistoryProgress: self._progress.setMaximum(0) # unknown self._progress.setMinimumDuration(0) self._progress.setLabelText(text) - self._progress.setCancelButton(None) + + no_button = None + if machinery.IS_QT6: + # FIXME:mypy PyQt6 stubs issue + no_button = cast(QPushButton, None) + + self._progress.setCancelButton(no_button) self._progress.setAutoClose(False) self._progress.show() QApplication.processEvents() diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index a2ce67750..ed0cae56f 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -28,7 +28,7 @@ from qutebrowser.qt.gui import QCloseEvent from qutebrowser.browser import eventfilter from qutebrowser.config import configfiles, config -from qutebrowser.utils import log, usertypes +from qutebrowser.utils import log, usertypes, qtutils from qutebrowser.keyinput import modeman from qutebrowser.misc import miscwidgets @@ -70,8 +70,9 @@ class _EventFilter(QObject): clicked = pyqtSignal() - def eventFilter(self, _obj: QObject, event: QEvent) -> bool: + def eventFilter(self, _obj: Optional[QObject], event: Optional[QEvent]) -> bool: """Translate mouse presses to a clicked signal.""" + assert event is not None if event.type() == QEvent.Type.MouseButtonPress: self.clicked.emit() return False @@ -162,7 +163,7 @@ class AbstractWebInspector(QWidget): self.shutdown() return elif position == Position.window: - self.setParent(None) # type: ignore[call-overload] + self.setParent(qtutils.QT_NONE) self._load_state_geometry() else: self._splitter.set_inspector(self, position) @@ -195,7 +196,7 @@ class AbstractWebInspector(QWidget): if not ok: log.init.warning("Error while loading geometry.") - def closeEvent(self, _e: QCloseEvent) -> None: + def closeEvent(self, _e: Optional[QCloseEvent]) -> None: """Save the geometry when closed.""" data = self._widget.saveGeometry().data() geom = base64.b64encode(data).decode('ASCII') diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index c66b6bc03..162e1c5d0 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -28,7 +28,7 @@ from qutebrowser.qt.network import (QNetworkProxy, QNetworkRequest, QHostInfo, QHostAddress) from qutebrowser.qt.qml import QJSEngine, QJSValue -from qutebrowser.utils import log, utils, qtutils, resources, urlutils +from qutebrowser.utils import log, qtlog, utils, qtutils, resources, urlutils class ParseProxyError(Exception): @@ -65,7 +65,8 @@ def _js_slot(*args): return self._error_con.callAsConstructor([e]) # pylint: enable=protected-access - deco = pyqtSlot(*args, result=QJSValue) + # FIXME:mypy PyQt6 stubs issue, passing type should work too + deco = pyqtSlot(*args, result="QJSValue") return deco(new_method) return _decorator @@ -257,7 +258,7 @@ class PACFetcher(QObject): url.setScheme(url.scheme()[len(pac_prefix):]) self._pac_url = url - with log.disable_qt_msghandler(): + with qtlog.disable_qt_msghandler(): # WORKAROUND for a hang when messages are printed, see our # NetworkAccessManager subclass for details. self._manager: Optional[QNetworkAccessManager] = QNetworkAccessManager() @@ -276,6 +277,7 @@ class PACFetcher(QObject): """Fetch the proxy from the remote URL.""" assert self._manager is not None self._reply = self._manager.get(QNetworkRequest(self._pac_url)) + assert self._reply is not None self._reply.finished.connect(self._finish) @pyqtSlot() diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index 714823d2c..53aaac38c 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -21,7 +21,7 @@ from qutebrowser.qt.core import QUrl, pyqtSlot from qutebrowser.qt.network import QNetworkProxy, QNetworkProxyFactory from qutebrowser.config import config, configtypes -from qutebrowser.utils import message, usertypes, urlutils, utils +from qutebrowser.utils import message, usertypes, urlutils, utils, qtutils from qutebrowser.misc import objects from qutebrowser.browser.network import pac @@ -51,7 +51,7 @@ def _warn_for_pac(): @pyqtSlot() def shutdown(): QNetworkProxyFactory.setApplicationProxyFactory( - None) # type: ignore[arg-type] + qtutils.QT_NONE) class ProxyFactory(QNetworkProxyFactory): diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index cd4a75351..0b20b3785 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -29,7 +29,7 @@ from qutebrowser.qt.widgets import QApplication from qutebrowser.qt.network import QNetworkRequest, QNetworkReply, QNetworkAccessManager from qutebrowser.config import config, websettings -from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg +from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg, qtlog from qutebrowser.misc import quitter from qutebrowser.browser import downloads from qutebrowser.browser.webkit import http @@ -121,7 +121,7 @@ class DownloadItem(downloads.AbstractDownloadItem): self._reply.errorOccurred.disconnect() self._reply.readyRead.disconnect() - with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal ' + with qtlog.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal ' 'problem, this method must only be called ' 'once.'): # See https://codereview.qt-project.org/#/c/107863/ diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 75acb3252..f56db3a65 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -33,9 +33,8 @@ class WebEngineRequest(interceptors.Request): """QtWebEngine-specific request interceptor functionality.""" _WHITELISTED_REQUEST_METHODS = { - # FIXME:mypy PyQt6-stubs issue? - QByteArray(b'GET'), # type: ignore[call-overload,unused-ignore] - QByteArray(b'HEAD'), # type: ignore[call-overload,unused-ignore] + QByteArray(b'GET'), + QByteArray(b'HEAD'), } def __init__(self, *args, webengine_info, **kwargs): diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py index d140a8c61..d101c616c 100644 --- a/qutebrowser/browser/webengine/notification.py +++ b/qutebrowser/browser/webengine/notification.py @@ -1108,9 +1108,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter): if padding and self._quirks.no_padded_images: return None - bits = qimage.constBits().asstring(size) - # FIXME:mypy PyQt6-stubs issue - image_data.add(QByteArray(bits)) # type: ignore[call-overload,unused-ignore] + bits_ptr = qimage.constBits() + assert bits_ptr is not None + bits = bits_ptr.asstring(size) + image_data.add(QByteArray(bits)) image_data.endStructure() return image_data diff --git a/qutebrowser/browser/webengine/tabhistory.py b/qutebrowser/browser/webengine/tabhistory.py index 30f3facdb..2848142ef 100644 --- a/qutebrowser/browser/webengine/tabhistory.py +++ b/qutebrowser/browser/webengine/tabhistory.py @@ -153,6 +153,8 @@ def serialize(items): for item in items: _serialize_item(item, stream) - stream.device().reset() + dev = stream.device() + assert dev is not None + dev.reset() qtutils.check_qdatastream(stream) return stream, data, cur_user_data diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 4a09c81fb..010b00975 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -25,8 +25,7 @@ from qutebrowser.qt.webenginecore import (QWebEngineUrlSchemeHandler, from qutebrowser.browser import qutescheme from qutebrowser.utils import log, qtutils -# FIXME:mypy PyQt6-stubs issue? -_QUTE = QByteArray(b'qute') # type: ignore[call-overload,unused-ignore] +_QUTE = QByteArray(b'qute') class QuteSchemeHandler(QWebEngineUrlSchemeHandler): diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 9d58c0379..ade008143 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -54,7 +54,9 @@ class WebEngineView(QWebEngineView): self._win_id = win_id self._tabdata = tabdata - theme_color = self.style().standardPalette().color(QPalette.ColorRole.Base) + style = self.style() + assert style is not None + theme_color = style.standardPalette().color(QPalette.ColorRole.Base) if private: assert webenginesettings.private_profile is not None profile = webenginesettings.private_profile diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 91e609456..4c1c767ec 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -28,7 +28,7 @@ from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkReply, QSslCo from qutebrowser.config import config from qutebrowser.utils import (message, log, usertypes, utils, objreg, - urlutils, debug) + urlutils, debug, qtlog) from qutebrowser.browser import shared from qutebrowser.browser.network import proxy as proxymod from qutebrowser.extensions import interceptors @@ -156,7 +156,7 @@ class NetworkManager(QNetworkAccessManager): def __init__(self, *, win_id, tab_id, private, parent=None): log.init.debug("Initializing NetworkManager") - with log.disable_qt_msghandler(): + with qtlog.disable_qt_msghandler(): # WORKAROUND for a hang when a message is printed - See: # https://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html # diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index e497e1204..cc5859ca6 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -291,6 +291,7 @@ class CompletionItemDelegate(QStyledItemDelegate): self._opt = QStyleOptionViewItem(option) self.initStyleOption(self._opt, index) self._style = self._opt.widget.style() + assert self._style is not None self._get_textdoc(index) assert self._doc is not None diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 01527e763..665757e89 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -156,6 +156,15 @@ class CompletionView(QTreeView): assert isinstance(model, completionmodel.CompletionModel), model return model + def _selection_model(self) -> QItemSelectionModel: + """Get the current selection model. + + Ensures the model is not None. + """ + model = self.selectionModel() + assert model is not None + return model + @pyqtSlot(str) def _on_config_changed(self, option): if option in ['completion.height', 'completion.shrink']: @@ -169,7 +178,9 @@ class CompletionView(QTreeView): column_widths = self._model().column_widths pixel_widths = [(width * perc // 100) for perc in column_widths] - delta = self.verticalScrollBar().sizeHint().width() + bar = self.verticalScrollBar() + assert bar is not None + delta = bar.sizeHint().width() for i, width in reversed(list(enumerate(pixel_widths))): if width > delta: pixel_widths[i] -= delta @@ -191,7 +202,7 @@ class CompletionView(QTreeView): A QModelIndex. """ model = self._model() - idx = self.selectionModel().currentIndex() + idx = self._selection_model().currentIndex() if not idx.isValid(): # No item selected yet if upwards: @@ -223,7 +234,7 @@ class CompletionView(QTreeView): Return: A QModelIndex. """ - old_idx = self.selectionModel().currentIndex() + old_idx = self._selection_model().currentIndex() idx = old_idx model = self._model() @@ -267,7 +278,7 @@ class CompletionView(QTreeView): Return: A QModelIndex. """ - idx = self.selectionModel().currentIndex() + idx = self._selection_model().currentIndex() model = self._model() if not idx.isValid(): return self._next_idx(upwards).sibling(0, 0) @@ -323,7 +334,7 @@ class CompletionView(QTreeView): if not self._active: return - selmodel = self.selectionModel() + selmodel = self._selection_model() indices = { 'next': lambda: self._next_idx(upwards=False), 'prev': lambda: self._next_idx(upwards=True), @@ -363,9 +374,10 @@ class CompletionView(QTreeView): Args: model: The model to use. """ - if self.model() is not None and model is not self.model(): - self.model().deleteLater() - self.selectionModel().deleteLater() + old_model = self.model() + if old_model is not None and model is not old_model: + old_model.deleteLater() + self._selection_model().deleteLater() self.setModel(model) @@ -395,7 +407,7 @@ class CompletionView(QTreeView): self.pattern = pattern with debug.log_time(log.completion, 'Set pattern {}'.format(pattern)): self._model().set_pattern(pattern) - self.selectionModel().clear() + self._selection_model().clear() self._maybe_update_geometry() self._maybe_show() @@ -415,7 +427,7 @@ class CompletionView(QTreeView): def on_clear_completion_selection(self): """Clear the selection model when an item is activated.""" self.hide() - selmod = self.selectionModel() + selmod = self._selection_model() if selmod is not None: selmod.clearSelection() selmod.clearCurrentIndex() @@ -426,14 +438,18 @@ class CompletionView(QTreeView): confheight = str(config.val.completion.height) if confheight.endswith('%'): perc = int(confheight.rstrip('%')) - height = self.window().height() * perc // 100 + window = self.window() + assert window is not None + height = window.height() * perc // 100 else: height = int(confheight) # Shrink to content size if needed and shrinking is enabled if config.val.completion.shrink: + bar = self.horizontalScrollBar() + assert bar is not None contents_height = ( self.viewportSizeHint().height() + - self.horizontalScrollBar().sizeHint().height()) + bar.sizeHint().height()) if contents_height <= height: height = contents_height # The width isn't really relevant as we're expanding anyways. diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 8a255d04c..0b9d669dc 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1741,6 +1741,7 @@ hints.selectors: - '[role="menuitemcheckbox"]' - '[role="menuitemradio"]' - '[role="treeitem"]' + - '[aria-haspopup]' - '[ng-click]' - '[ngClick]' - '[data-ng-click]' diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 2a13133ae..ac593cfae 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -108,7 +108,9 @@ class StateConfig(configparser.ConfigParser): for sect, key in deleted_keys: self[sect].pop(key, None) - self['general']['qt_version'] = qVersion() + qt_version = qVersion() + assert qt_version is not None + self['general']['qt_version'] = qt_version self['general']['qtwe_version'] = self._qtwe_version_str() self['general']['chromium_version'] = self._chromium_version_str() self['general']['version'] = qutebrowser.__version__ diff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py index 007be6d15..40581b3c1 100644 --- a/qutebrowser/keyinput/eventfilter.py +++ b/qutebrowser/keyinput/eventfilter.py @@ -17,9 +17,8 @@ """Global Qt event filter which dispatches key events.""" -from typing import cast +from typing import cast, Optional -from qutebrowser.qt import machinery from qutebrowser.qt.core import pyqtSlot, QObject, QEvent from qutebrowser.qt.gui import QKeyEvent, QWindow @@ -76,7 +75,7 @@ class EventFilter(QObject): # No window available yet, or not a MainWindow return False - def eventFilter(self, obj: QObject, event: QEvent) -> bool: + def eventFilter(self, obj: Optional[QObject], event: Optional[QEvent]) -> bool: """Handle an event. Args: @@ -86,9 +85,8 @@ class EventFilter(QObject): Return: True if the event should be filtered, False if it's passed through. """ + assert event is not None ev_type = event.type() - if machinery.IS_QT6: - ev_type = cast(QEvent.Type, ev_type) if self._log_qt_events: try: diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index fe3650a2c..897318b66 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -474,11 +474,7 @@ class ModeManager(QObject): QEvent.Type.ShortcutOverride: functools.partial(self._handle_keypress, dry_run=True), } - ev_type = event.type() - if machinery.IS_QT6: - ev_type = cast(QEvent.Type, ev_type) - - handler = handlers[ev_type] + handler = handlers[event.type()] return handler(cast(QKeyEvent, event)) @cmdutils.register(instance='mode-manager', scope='window') diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 26a2ae886..17772b2ea 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -303,6 +303,7 @@ class PromptContainer(QWidget): item = self._layout.takeAt(0) if item is not None: widget = item.widget() + assert widget is not None log.prompt.debug("Deleting old prompt {}".format(widget)) widget.hide() widget.deleteLater() @@ -366,6 +367,7 @@ class PromptContainer(QWidget): item = self._layout.takeAt(0) if item is not None: widget = item.widget() + assert widget is not None log.prompt.debug("Deleting prompt {}".format(widget)) widget.hide() widget.deleteLater() @@ -780,6 +782,7 @@ class FilenamePrompt(_BasePrompt): # This duplicates some completion code, but I don't see a nicer way... assert which in ['prev', 'next'], which selmodel = self._file_view.selectionModel() + assert selmodel is not None parent = self._file_view.rootIndex() first_index = self._file_model.index(0, 0, parent) diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index 4332316a3..68bacd0b0 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -259,15 +259,19 @@ class Command(misc.CommandLineEdit): else: raise utils.Unreachable("setText got called with invalid text " "'{}'!".format(text)) + # FIXME:mypy PyQt6 stubs issue + if machinery.IS_QT6: + text = cast(str, text) super().setText(text) - def keyPressEvent(self, e: QKeyEvent) -> None: + def keyPressEvent(self, e: Optional[QKeyEvent]) -> None: """Override keyPressEvent to ignore Return key presses, and add Shift-Ins. If this widget is focused, we are in passthrough key mode, and Enter/Shift+Enter/etc. will cause QLineEdit to think it's finished without command_accept to be called. """ + assert e is not None if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing shift = cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier) else: diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index 54faf232d..7892b3e83 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -116,7 +116,9 @@ class UrlText(textbase.TextBase): if old_urltype != self._urltype: # We can avoid doing an unpolish here because the new style will # always override the old one. - self.style().polish(self) + style = self.style() + assert style is not None + style.polish(self) @pyqtSlot(usertypes.LoadStatus) def on_load_status_changed(self, status): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index da3392a7e..e597c9efe 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -249,7 +249,7 @@ class TabbedBrowser(QWidget): self.search_options: Mapping[str, Any] = {} self._local_marks: MutableMapping[QUrl, MutableMapping[str, QPoint]] = {} self._global_marks: MutableMapping[str, Tuple[QPoint, QUrl]] = {} - self.default_window_icon = self.widget.window().windowIcon() + self.default_window_icon = self._window().windowIcon() self.is_private = private self.tab_deque = TabDeque() config.instance.changed.connect(self._on_config_changed) @@ -301,10 +301,9 @@ class TabbedBrowser(QWidget): """ widgets = [] for i in range(self.widget.count()): - widget = self.widget.widget(i) + widget = qtutils.add_optional(self.widget.widget(i)) if widget is None: - log.webview.debug( # type: ignore[unreachable] - "Got None-widget in tabbedbrowser!") + log.webview.debug("Got None-widget in tabbedbrowser!") else: widgets.append(widget) return widgets @@ -330,7 +329,8 @@ class TabbedBrowser(QWidget): fields['id'] = self._win_id title = title_format.format(**fields) - self.widget.window().setWindowTitle(title) + + self._window().setWindowTitle(title) def _connect_tab_signals(self, tab): """Set up the needed signals for tab.""" @@ -396,6 +396,15 @@ class TabbedBrowser(QWidget): assert isinstance(tab, browsertab.AbstractTab), tab return tab + def _window(self) -> QWidget: + """Get the current window widget. + + Note: This asserts if there is no window. + """ + window = self.widget.window() + assert window is not None + return window + def _tab_by_idx(self, idx: int) -> Optional[browsertab.AbstractTab]: """Get a browser tab by index. @@ -662,11 +671,12 @@ class TabbedBrowser(QWidget): # Make sure the background tab has the correct initial size. # With a foreground tab, it's going to be resized correctly by the # layout anyways. - tab.resize(self.widget.currentWidget().size()) + current_widget = self._current_tab() + tab.resize(current_widget.size()) self.widget.tab_index_changed.emit(self.widget.currentIndex(), self.widget.count()) # Refocus webview in case we lost it by spawning a bg tab - self.widget.currentWidget().setFocus() + current_widget.setFocus() else: self.widget.setCurrentWidget(tab) @@ -739,7 +749,7 @@ class TabbedBrowser(QWidget): tab.data.keep_icon = False elif (config.cache['tabs.tabs_are_windows'] and tab.data.should_show_icon()): - self.widget.window().setWindowIcon(self.default_window_icon) + self._window().setWindowIcon(self.default_window_icon) @pyqtSlot() def _on_load_status_changed(self, tab): @@ -862,9 +872,10 @@ class TabbedBrowser(QWidget): @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): """Give focus to current tab if command mode was left.""" - widget = self.widget.currentWidget() + widget = qtutils.add_optional(self.widget.currentWidget()) if widget is None: - return # type: ignore[unreachable] + return + if mode in [usertypes.KeyMode.command] + modeman.PROMPT_MODES: log.modes.debug("Left status-input mode, focusing {!r}".format( widget)) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index fe9ce1e06..150c820a8 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -355,7 +355,9 @@ class TabWidget(QTabWidget): self.setTabIcon(idx, icon) if config.val.tabs.tabs_are_windows: - self.window().setWindowIcon(tab.icon()) + window = self.window() + assert window is not None + window.setWindowIcon(tab.icon()) def setTabIcon(self, idx: int, icon: QIcon) -> None: """Always show tab icons for pinned tabs in some circumstances.""" @@ -365,7 +367,9 @@ class TabWidget(QTabWidget): config.cache['tabs.pinned.shrink'] and not self.tab_bar().vertical and tab is not None and tab.data.pinned): - icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon) + style = self.style() + assert style is not None + icon = style.standardIcon(QStyle.StandardPixmap.SP_FileIcon) super().setTabIcon(idx, icon) @@ -809,6 +813,12 @@ class TabBarStyle(QProxyStyle): ICON_PADDING = 4 + def _base_style(self) -> QStyle: + """Get the base style.""" + style = self.baseStyle() + assert style is not None + return style + def _draw_indicator(self, layouts, opt, p): """Draw the tab indicator. @@ -836,7 +846,7 @@ class TabBarStyle(QProxyStyle): icon_state = (QIcon.State.On if opt.state & QStyle.StateFlag.State_Selected else QIcon.State.Off) icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state) - self.baseStyle().drawItemPixmap(p, layouts.icon, Qt.AlignmentFlag.AlignCenter, icon) + self._base_style().drawItemPixmap(p, layouts.icon, Qt.AlignmentFlag.AlignCenter, icon) def drawControl(self, element, opt, p, widget=None): """Override drawControl to draw odd tabs in a different color. @@ -853,7 +863,7 @@ class TabBarStyle(QProxyStyle): if element not in [QStyle.ControlElement.CE_TabBarTab, QStyle.ControlElement.CE_TabBarTabShape, QStyle.ControlElement.CE_TabBarTabLabel]: # Let the real style draw it. - self.baseStyle().drawControl(element, opt, p, widget) + self._base_style().drawControl(element, opt, p, widget) return layouts = self._tab_layout(opt) @@ -876,7 +886,7 @@ class TabBarStyle(QProxyStyle): self._draw_icon(layouts, opt, p) alignment = (config.cache['tabs.title.alignment'] | Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextHideMnemonic) - self.baseStyle().drawItemText( + self._base_style().drawItemText( p, layouts.text, int(alignment), @@ -906,7 +916,7 @@ class TabBarStyle(QProxyStyle): QStyle.PixelMetric.PM_TabBarScrollButtonWidth]: return 0 else: - return self.baseStyle().pixelMetric(metric, option, widget) + return self._base_style().pixelMetric(metric, option, widget) def subElementRect(self, sr, opt, widget=None): """Override subElementRect to use our own _tab_layout implementation. @@ -936,7 +946,7 @@ class TabBarStyle(QProxyStyle): # style differences... return QCommonStyle.subElementRect(self, sr, opt, widget) else: - return self.baseStyle().subElementRect(sr, opt, widget) + return self._base_style().subElementRect(sr, opt, widget) def _tab_layout(self, opt): """Compute the text/icon rect from the opt rect. @@ -983,7 +993,7 @@ class TabBarStyle(QProxyStyle): text_rect.adjust( icon_rect.width() + TabBarStyle.ICON_PADDING, 0, 0, 0) - text_rect = self.baseStyle().visualRect(opt.direction, opt.rect, text_rect) + text_rect = self._base_style().visualRect(opt.direction, opt.rect, text_rect) return Layouts(text=text_rect, icon=icon_rect, indicator=indicator_rect) @@ -1018,5 +1028,5 @@ class TabBarStyle(QProxyStyle): icon_top = text_rect.center().y() + 1 - tab_icon_size.height() // 2 icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size) - icon_rect = self.baseStyle().visualRect(opt.direction, opt.rect, icon_rect) + icon_rect = self._base_style().visualRect(opt.direction, opt.rect, icon_rect) return icon_rect diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 6d8d9916f..641798190 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -125,6 +125,7 @@ class ConsoleTextEdit(QTextEdit): self.moveCursor(QTextCursor.MoveOperation.End) self.insertPlainText(text) scrollbar = self.verticalScrollBar() + assert scrollbar is not None scrollbar.setValue(scrollbar.maximum()) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 416072ccb..04c92a529 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -631,8 +631,7 @@ class ReportErrorDialog(QDialog): hbox = QHBoxLayout() hbox.addStretch() btn = QPushButton("Close") - # FIXME:mypy PyQt6-stubs issue - btn.clicked.connect(self.close) # type: ignore[arg-type,unused-ignore] + btn.clicked.connect(self.close) hbox.addWidget(btn) vbox.addLayout(hbox) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index a0265d653..57e821784 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -298,7 +298,17 @@ def init_log(args): from qutebrowser.utils import log log.init_log(args) log.init.debug("Log initialized.") - log.init.debug(str(machinery.INFO)) + + +def init_qtlog(args): + """Initialize Qt logging. + + Args: + args: The argparse namespace. + """ + from qutebrowser.utils import log, qtlog + qtlog.init(args) + log.init.debug("Qt log initialized.") def check_optimize_flag(): @@ -333,16 +343,18 @@ def early_init(args): Args: args: The argparse namespace. """ + # Init logging as early as possible + init_log(args) # First we initialize the faulthandler as early as possible, so we # theoretically could catch segfaults occurring later during earlyinit. init_faulthandler() # Then we configure the selected Qt wrapper info = machinery.init(args) + # Init Qt logging after machinery is initialized + init_qtlog(args) # Here we check if QtCore is available, and if not, print a message to the # console or via Tk. check_qt_available(info) - # Init logging as early as possible - init_log(args) # Now we can be sure QtCore is available, so we can print dialogs on # errors, so people only using the GUI notice them as well. check_libraries() diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index a50849d29..ac7290ef4 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -27,7 +27,7 @@ from typing import Mapping, Sequence, Dict, Optional from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, QObject, QProcess, QProcessEnvironment, QByteArray, QUrl, Qt) -from qutebrowser.utils import message, log, utils, usertypes, version +from qutebrowser.utils import message, log, utils, usertypes, version, qtutils from qutebrowser.api import cmdutils, apitypes from qutebrowser.completion.models import miscmodels @@ -394,7 +394,7 @@ class GUIProcess(QObject): log.procs.debug("Starting process.") self._pre_start(cmd, args) self._proc.start( - self.resolved_cmd, # type: ignore[arg-type] + qtutils.remove_optional(self.resolved_cmd), args, ) self._post_start() diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py index 1dddddba7..45d491996 100644 --- a/qutebrowser/misc/httpclient.py +++ b/qutebrowser/misc/httpclient.py @@ -25,7 +25,7 @@ from qutebrowser.qt.core import pyqtSignal, QObject, QTimer from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkRequest, QNetworkReply) -from qutebrowser.utils import log +from qutebrowser.utils import qtlog class HTTPRequest(QNetworkRequest): @@ -59,7 +59,7 @@ class HTTPClient(QObject): def __init__(self, parent=None): super().__init__(parent) - with log.disable_qt_msghandler(): + with qtlog.disable_qt_msghandler(): # WORKAROUND for a hang when messages are printed, see our # NetworkAccessManager subclass for details. self._nam = QNetworkAccessManager(self) @@ -78,9 +78,7 @@ class HTTPClient(QObject): request = HTTPRequest(url) request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, 'application/x-www-form-urlencoded;charset=utf-8') - # FIXME:mypy PyQt6-stubs issue - reply = self._nam.post( # type: ignore[call-overload,unused-ignore] - request, encoded_data) + reply = self._nam.post(request, encoded_data) self._handle_reply(reply) def get(self, url): diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index fb1b1ac22..b809394f1 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -28,7 +28,7 @@ from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, Qt from qutebrowser.qt.network import QLocalSocket, QLocalServer, QAbstractSocket import qutebrowser -from qutebrowser.utils import log, usertypes, error, standarddir, utils, debug +from qutebrowser.utils import log, usertypes, error, standarddir, utils, debug, qtutils from qutebrowser.qt import sip @@ -259,10 +259,9 @@ class IPCServer(QObject): "still handling another one (0x{:x}).".format( id(self._socket))) return - socket = self._server.nextPendingConnection() + socket = qtutils.add_optional(self._server.nextPendingConnection()) if socket is None: - log.ipc.debug( # type: ignore[unreachable] - "No new connection to handle.") + log.ipc.debug("No new connection to handle.") return log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket))) self._socket = socket diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index ba33da775..1e90ac75a 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -25,7 +25,7 @@ from qutebrowser.qt.widgets import (QLineEdit, QWidget, QHBoxLayout, QLabel, from qutebrowser.qt.gui import QValidator, QPainter, QResizeEvent from qutebrowser.config import config, configfiles -from qutebrowser.utils import utils, log, usertypes, debug +from qutebrowser.utils import utils, log, usertypes, debug, qtutils from qutebrowser.misc import cmdhistory from qutebrowser.browser import inspector from qutebrowser.keyinput import keyutils, modeman @@ -185,7 +185,10 @@ class _FoldArrow(QWidget): elem = QStyle.PrimitiveElement.PE_IndicatorArrowRight else: elem = QStyle.PrimitiveElement.PE_IndicatorArrowDown - self.style().drawPrimitive(elem, opt, painter, self) + + style = self.style() + assert style is not None + style.drawPrimitive(elem, opt, painter, self) def minimumSizeHint(self): """Return a sensible size.""" @@ -241,10 +244,10 @@ class WrapperLayout(QLayout): if self._widget is None: return assert self._container is not None - self._widget.setParent(None) # type: ignore[call-overload] + self._widget.setParent(qtutils.QT_NONE) self._widget.deleteLater() self._widget = None - self._container.setFocusProxy(None) # type: ignore[arg-type] + self._container.setFocusProxy(qtutils.QT_NONE) class FullscreenNotification(QLabel): @@ -270,9 +273,17 @@ class FullscreenNotification(QLabel): self.resize(self.sizeHint()) if config.val.content.fullscreen.window: - geom = self.parentWidget().geometry() + parent = self.parentWidget() + assert parent is not None + geom = parent.geometry() else: - geom = self.window().windowHandle().screen().geometry() + window = self.window() + assert window is not None + handle = window.windowHandle() + assert handle is not None + screen = handle.screen() + assert screen is not None + geom = screen.geometry() self.move((geom.width() - self.sizeHint().width()) // 2, 30) def set_timeout(self, timeout): @@ -327,6 +338,8 @@ class InspectorSplitter(QSplitter): main_widget = self.widget(self._main_idx) inspector_widget = self.widget(self._inspector_idx) + assert main_widget is not None + assert inspector_widget is not None if not inspector_widget.isVisible(): raise inspector.Error("No inspector inside main window") @@ -439,8 +452,9 @@ class InspectorSplitter(QSplitter): self._preferred_size = sizes[self._inspector_idx] self._save_preferred_size() - def resizeEvent(self, e: QResizeEvent) -> None: + def resizeEvent(self, e: Optional[QResizeEvent]) -> None: """Window resize event.""" + assert e is not None super().resizeEvent(e) if self.count() == 2: self._adjust_size() diff --git a/qutebrowser/misc/nativeeventfilter.py b/qutebrowser/misc/nativeeventfilter.py index 4562ea82d..5fad3359c 100644 --- a/qutebrowser/misc/nativeeventfilter.py +++ b/qutebrowser/misc/nativeeventfilter.py @@ -20,7 +20,7 @@ This entire file is a giant WORKAROUND for https://bugreports.qt.io/browse/QTBUG-114334. """ -from typing import Tuple, Union, cast +from typing import Tuple, Union, cast, Optional import enum import ctypes import ctypes.util @@ -150,11 +150,12 @@ class NativeEventFilter(QAbstractNativeEventFilter): xcb.xcb_disconnect(conn) def nativeEventFilter( - self, evtype: Union[bytes, QByteArray], message: sip.voidptr + self, evtype: Union[bytes, QByteArray], message: Optional[sip.voidptr] ) -> Tuple[bool, _PointerRetType]: """Handle XCB events.""" # We're only installed when the platform plugin is xcb assert evtype == b"xcb_generic_event_t", evtype + assert message is not None # We cast to xcb_ge_generic_event_t, which overlaps with xcb_generic_event_t. # .extension and .event_type will only make sense if this is an diff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py index d502efa65..59e2d552f 100644 --- a/qutebrowser/misc/quitter.py +++ b/qutebrowser/misc/quitter.py @@ -37,7 +37,7 @@ except ImportError: import qutebrowser from qutebrowser.api import cmdutils -from qutebrowser.utils import log +from qutebrowser.utils import log, qtlog from qutebrowser.misc import sessions, ipc, objects from qutebrowser.mainwindow import prompt from qutebrowser.completion.models import miscmodels @@ -304,5 +304,5 @@ def init(args: argparse.Namespace) -> None: """Initialize the global Quitter instance.""" global instance instance = Quitter(args=args, parent=objects.qapp) - instance.shutting_down.connect(log.shutdown_log) + instance.shutting_down.connect(qtlog.shutdown_log) objects.qapp.lastWindowClosed.connect(instance.on_last_window_closed) diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index 0aac9005c..abdfd0eba 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -201,10 +201,10 @@ def simple_split(s, keep=False, maxsplit=None): if keep: pattern = '([' + whitespace + '])' - parts = re.split(pattern, s, maxsplit) + parts = re.split(pattern, s, maxsplit=maxsplit) return _combine_ws(parts, whitespace) else: pattern = '[' + whitespace + ']' - parts = re.split(pattern, s, maxsplit) + parts = re.split(pattern, s, maxsplit=maxsplit) parts[-1] = parts[-1].rstrip() return [p for p in parts if p] diff --git a/qutebrowser/qt/_core_pyqtproperty.py b/qutebrowser/qt/_core_pyqtproperty.py index bdf7013e2..ae6435039 100644 --- a/qutebrowser/qt/_core_pyqtproperty.py +++ b/qutebrowser/qt/_core_pyqtproperty.py @@ -1,16 +1,18 @@ """WORKAROUND for missing pyqtProperty typing, ported from PyQt5-stubs: FIXME:mypy PyQt6-stubs issue -https://github.com/python-qt-tools/PyQt5-stubs/blob/5.15.6.0/PyQt5-stubs/QtCore.pyi#L70-L111 +https://github.com/python-qt-tools/PyQt5-stubs/blob/5.15.6.0/PyQt5-stubs/QtCore.pyi#L68-L111 """ # flake8: noqa # pylint: disable=invalid-name,missing-class-docstring,too-many-arguments,redefined-builtin,unused-argument,no-name-in-module import typing -from PyQt6.QtCore import QObjectT, pyqtSignal +from PyQt6.QtCore import QObject, pyqtSignal if typing.TYPE_CHECKING: + QObjectT = typing.TypeVar("QObjectT", bound=QObject) + TPropertyTypeVal = typing.TypeVar("TPropertyTypeVal") TPropGetter = typing.TypeVar( diff --git a/qutebrowser/qt/machinery.py b/qutebrowser/qt/machinery.py index 39d8661bf..616c7ccfc 100644 --- a/qutebrowser/qt/machinery.py +++ b/qutebrowser/qt/machinery.py @@ -28,6 +28,8 @@ import importlib import dataclasses from typing import Optional, Dict +from qutebrowser.utils import log + # Packagers: Patch the line below to enforce a Qt wrapper, e.g.: # sed -i 's/_WRAPPER_OVERRIDE = .*/_WRAPPER_OVERRIDE = "PyQt6"/' qutebrowser/qt/machinery.py # @@ -294,6 +296,7 @@ def init(args: argparse.Namespace) -> SelectionInfo: info = _select_wrapper(args) if info.wrapper is not None: _set_globals(info) + log.init.debug(str(info)) # If info is None here (no Qt wrapper available), we'll show an error later # in earlyinit.py. diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index a8b436d79..82de30702 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -68,6 +68,7 @@ def log_signals(obj: QObject) -> QObject: def connect_log_slot(obj: QObject) -> None: """Helper function to connect all signals to a logging slot.""" metaobj = obj.metaObject() + assert metaobj is not None for i in range(metaobj.methodCount()): meta_method = metaobj.method(i) qtutils.ensure_valid(meta_method) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 521f52b5b..f2a6c396d 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -24,8 +24,6 @@ import logging import contextlib import collections import copy -import faulthandler -import traceback import warnings import json import inspect @@ -33,7 +31,9 @@ import argparse from typing import (TYPE_CHECKING, Any, Iterator, Mapping, MutableSequence, Optional, Set, Tuple, Union, TextIO, Literal, cast) -from qutebrowser.qt import core as qtcore +# NOTE: This is a Qt-free zone! All imports related to Qt logging should be done in +# qutebrowser.utils.qtlog (see https://github.com/qutebrowser/qutebrowser/issues/7769). + # Optional imports try: import colorama @@ -208,15 +208,9 @@ def init_log(args: argparse.Namespace) -> None: root.setLevel(logging.NOTSET) logging.captureWarnings(True) _init_py_warnings() - qtcore.qInstallMessageHandler(qt_message_handler) _log_inited = True -@qtcore.pyqtSlot() -def shutdown_log() -> None: - qtcore.qInstallMessageHandler(None) - - def _init_py_warnings() -> None: """Initialize Python warning handling.""" assert _args is not None @@ -231,16 +225,6 @@ def _init_py_warnings() -> None: @contextlib.contextmanager -def disable_qt_msghandler() -> Iterator[None]: - """Contextmanager which temporarily disables the Qt message handler.""" - old_handler = qtcore.qInstallMessageHandler(None) - try: - yield - finally: - qtcore.qInstallMessageHandler(old_handler) - - -@contextlib.contextmanager def py_warning_filter( action: Literal['default', 'error', 'ignore', 'always', 'module', 'once'] = 'ignore', @@ -377,163 +361,6 @@ def change_console_formatter(level: int) -> None: assert isinstance(old_formatter, JSONFormatter), old_formatter -def qt_message_handler(msg_type: qtcore.QtMsgType, - context: qtcore.QMessageLogContext, - msg: str) -> None: - """Qt message handler to redirect qWarning etc. to the logging system. - - Args: - msg_type: The level of the message. - context: The source code location of the message. - msg: The message text. - """ - # Mapping from Qt logging levels to the matching logging module levels. - # Note we map critical to ERROR as it's actually "just" an error, and fatal - # to critical. - qt_to_logging = { - qtcore.QtMsgType.QtDebugMsg: logging.DEBUG, - qtcore.QtMsgType.QtWarningMsg: logging.WARNING, - qtcore.QtMsgType.QtCriticalMsg: logging.ERROR, - qtcore.QtMsgType.QtFatalMsg: logging.CRITICAL, - qtcore.QtMsgType.QtInfoMsg: logging.INFO, - } - - # Change levels of some well-known messages to debug so they don't get - # shown to the user. - # - # If a message starts with any text in suppressed_msgs, it's not logged as - # error. - suppressed_msgs = [ - # PNGs in Qt with broken color profile - # https://bugreports.qt.io/browse/QTBUG-39788 - ('libpng warning: iCCP: Not recognizing known sRGB profile that has ' - 'been edited'), - 'libpng warning: iCCP: known incorrect sRGB profile', - # Hopefully harmless warning - 'OpenType support missing for script ', - # Error if a QNetworkReply gets two different errors set. Harmless Qt - # bug on some pages. - # https://bugreports.qt.io/browse/QTBUG-30298 - ('QNetworkReplyImplPrivate::error: Internal problem, this method must ' - 'only be called once.'), - # Sometimes indicates missing text, but most of the time harmless - 'load glyph failed ', - # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479 - ('content-type missing in HTTP POST, defaulting to ' - 'application/x-www-form-urlencoded. ' - 'Use QNetworkRequest::setHeader() to fix this problem.'), - # https://bugreports.qt.io/browse/QTBUG-43118 - 'Using blocking call!', - # Hopefully harmless - ('"Method "GetAll" with signature "s" on interface ' - '"org.freedesktop.DBus.Properties" doesn\'t exist'), - ('"Method \\"GetAll\\" with signature \\"s\\" on interface ' - '\\"org.freedesktop.DBus.Properties\\" doesn\'t exist\\n"'), - 'WOFF support requires QtWebKit to be built with zlib support.', - # Weird Enlightment/GTK X extensions - 'QXcbWindow: Unhandled client message: "_E_', - 'QXcbWindow: Unhandled client message: "_ECORE_', - 'QXcbWindow: Unhandled client message: "_GTK_', - # Happens on AppVeyor CI - 'SetProcessDpiAwareness failed:', - # https://bugreports.qt.io/browse/QTBUG-49174 - ('QObject::connect: Cannot connect (null)::stateChanged(' - 'QNetworkSession::State) to ' - 'QNetworkReplyHttpImpl::_q_networkSessionStateChanged(' - 'QNetworkSession::State)'), - # https://bugreports.qt.io/browse/QTBUG-53989 - ("Image of format '' blocked because it is not considered safe. If " - "you are sure it is safe to do so, you can white-list the format by " - "setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST="), - # Installing Qt from the installer may cause it looking for SSL3 or - # OpenSSL 1.0 which may not be available on the system - "QSslSocket: cannot resolve ", - "QSslSocket: cannot call unresolved function ", - # When enabling debugging with QtWebEngine - ("Remote debugging server started successfully. Try pointing a " - "Chromium-based browser to "), - # https://github.com/qutebrowser/qutebrowser/issues/1287 - "QXcbClipboard: SelectionRequest too old", - # https://github.com/qutebrowser/qutebrowser/issues/2071 - 'QXcbWindow: Unhandled client message: ""', - # https://codereview.qt-project.org/176831 - "QObject::disconnect: Unexpected null parameter", - # https://bugreports.qt.io/browse/QTBUG-76391 - "Attribute Qt::AA_ShareOpenGLContexts must be set before " - "QCoreApplication is created.", - # Qt 6.4 beta 1: https://bugreports.qt.io/browse/QTBUG-104741 - "GL format 0 is not supported", - ] - # not using utils.is_mac here, because we can't be sure we can successfully - # import the utils module here. - if sys.platform == 'darwin': - suppressed_msgs += [ - # https://bugreports.qt.io/browse/QTBUG-47154 - ('virtual void QSslSocketBackendPrivate::transmit() SSLRead ' - 'failed with: -9805'), - ] - - if not msg: - msg = "Logged empty message!" - - if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): - level = logging.DEBUG - elif context.category == "qt.webenginecontext" and ( - msg.strip().startswith("GL Type: ") or # Qt 6.3 - msg.strip().startswith("GLImplementation:") # Qt 6.2 - ): - level = logging.DEBUG - else: - level = qt_to_logging[msg_type] - - if context.line is None: - lineno = -1 # type: ignore[unreachable] - else: - lineno = context.line - - if context.function is None: - func = 'none' # type: ignore[unreachable] - elif ':' in context.function: - func = '"{}"'.format(context.function) - else: - func = context.function - - if context.category is None or context.category == 'default': - name = 'qt' - else: - name = 'qt-' + context.category - if msg.splitlines()[0] == ('This application failed to start because it ' - 'could not find or load the Qt platform plugin ' - '"xcb".'): - # Handle this message specially. - msg += ("\n\nOn Archlinux, this should fix the problem:\n" - " pacman -S libxkbcommon-x11") - faulthandler.disable() - - assert _args is not None - if _args.debug: - stack: Optional[str] = ''.join(traceback.format_stack()) - else: - stack = None - - record = qt.makeRecord(name=name, level=level, fn=context.file, lno=lineno, - msg=msg, args=(), exc_info=None, func=func, - sinfo=stack) - qt.handle(record) - - -@contextlib.contextmanager -def hide_qt_warning(pattern: str, logger: str = 'qt') -> Iterator[None]: - """Hide Qt warnings matching the given regex.""" - log_filter = QtWarningFilter(pattern) - logger_obj = logging.getLogger(logger) - logger_obj.addFilter(log_filter) - try: - yield - finally: - logger_obj.removeFilter(log_filter) - - def init_from_config(conf: 'configmodule.ConfigContainer') -> None: """Initialize logging settings from the config. @@ -564,24 +391,6 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -> None: change_console_formatter(level) -class QtWarningFilter(logging.Filter): - - """Filter to filter Qt warnings. - - Attributes: - _pattern: The start of the message. - """ - - def __init__(self, pattern: str) -> None: - super().__init__() - self._pattern = pattern - - def filter(self, record: logging.LogRecord) -> bool: - """Determine if the specified record is to be logged.""" - do_log = not record.msg.strip().startswith(self._pattern) - return do_log - - class InvalidLogFilterError(Exception): """Raised when an invalid filter string is passed to LogFilter.parse().""" diff --git a/qutebrowser/utils/qtlog.py b/qutebrowser/utils/qtlog.py new file mode 100644 index 000000000..15e124b79 --- /dev/null +++ b/qutebrowser/utils/qtlog.py @@ -0,0 +1,241 @@ +# Copyright 2014-2023 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. + +"""Loggers and utilities related to Qt logging.""" + +import argparse +import contextlib +import faulthandler +import logging +import sys +import traceback +from typing import Iterator, Optional, Callable, cast + +from qutebrowser.qt import core as qtcore, machinery +from qutebrowser.utils import log + +_args = None + + +def init(args: argparse.Namespace) -> None: + """Install Qt message handler based on the argparse namespace passed.""" + global _args + _args = args + qtcore.qInstallMessageHandler(qt_message_handler) + + +@qtcore.pyqtSlot() +def shutdown_log() -> None: + qtcore.qInstallMessageHandler(None) + + +@contextlib.contextmanager +def disable_qt_msghandler() -> Iterator[None]: + """Contextmanager which temporarily disables the Qt message handler.""" + old_handler = qtcore.qInstallMessageHandler(None) + if machinery.IS_QT6: + # cast str to Optional[str] to be compatible with PyQt6 type hints for + # qInstallMessageHandler + old_handler = cast( + Optional[ + Callable[ + [qtcore.QtMsgType, qtcore.QMessageLogContext, Optional[str]], + None + ] + ], + old_handler, + ) + + try: + yield + finally: + qtcore.qInstallMessageHandler(old_handler) + + +def qt_message_handler(msg_type: qtcore.QtMsgType, + context: qtcore.QMessageLogContext, + msg: Optional[str]) -> None: + """Qt message handler to redirect qWarning etc. to the logging system. + + Args: + msg_type: The level of the message. + context: The source code location of the message. + msg: The message text. + """ + # Mapping from Qt logging levels to the matching logging module levels. + # Note we map critical to ERROR as it's actually "just" an error, and fatal + # to critical. + qt_to_logging = { + qtcore.QtMsgType.QtDebugMsg: logging.DEBUG, + qtcore.QtMsgType.QtWarningMsg: logging.WARNING, + qtcore.QtMsgType.QtCriticalMsg: logging.ERROR, + qtcore.QtMsgType.QtFatalMsg: logging.CRITICAL, + qtcore.QtMsgType.QtInfoMsg: logging.INFO, + } + + # Change levels of some well-known messages to debug so they don't get + # shown to the user. + # + # If a message starts with any text in suppressed_msgs, it's not logged as + # error. + suppressed_msgs = [ + # PNGs in Qt with broken color profile + # https://bugreports.qt.io/browse/QTBUG-39788 + ('libpng warning: iCCP: Not recognizing known sRGB profile that has ' + 'been edited'), + 'libpng warning: iCCP: known incorrect sRGB profile', + # Hopefully harmless warning + 'OpenType support missing for script ', + # Error if a QNetworkReply gets two different errors set. Harmless Qt + # bug on some pages. + # https://bugreports.qt.io/browse/QTBUG-30298 + ('QNetworkReplyImplPrivate::error: Internal problem, this method must ' + 'only be called once.'), + # Sometimes indicates missing text, but most of the time harmless + 'load glyph failed ', + # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479 + ('content-type missing in HTTP POST, defaulting to ' + 'application/x-www-form-urlencoded. ' + 'Use QNetworkRequest::setHeader() to fix this problem.'), + # https://bugreports.qt.io/browse/QTBUG-43118 + 'Using blocking call!', + # Hopefully harmless + ('"Method "GetAll" with signature "s" on interface ' + '"org.freedesktop.DBus.Properties" doesn\'t exist'), + ('"Method \\"GetAll\\" with signature \\"s\\" on interface ' + '\\"org.freedesktop.DBus.Properties\\" doesn\'t exist\\n"'), + 'WOFF support requires QtWebKit to be built with zlib support.', + # Weird Enlightment/GTK X extensions + 'QXcbWindow: Unhandled client message: "_E_', + 'QXcbWindow: Unhandled client message: "_ECORE_', + 'QXcbWindow: Unhandled client message: "_GTK_', + # Happens on AppVeyor CI + 'SetProcessDpiAwareness failed:', + # https://bugreports.qt.io/browse/QTBUG-49174 + ('QObject::connect: Cannot connect (null)::stateChanged(' + 'QNetworkSession::State) to ' + 'QNetworkReplyHttpImpl::_q_networkSessionStateChanged(' + 'QNetworkSession::State)'), + # https://bugreports.qt.io/browse/QTBUG-53989 + ("Image of format '' blocked because it is not considered safe. If " + "you are sure it is safe to do so, you can white-list the format by " + "setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST="), + # Installing Qt from the installer may cause it looking for SSL3 or + # OpenSSL 1.0 which may not be available on the system + "QSslSocket: cannot resolve ", + "QSslSocket: cannot call unresolved function ", + # When enabling debugging with QtWebEngine + ("Remote debugging server started successfully. Try pointing a " + "Chromium-based browser to "), + # https://github.com/qutebrowser/qutebrowser/issues/1287 + "QXcbClipboard: SelectionRequest too old", + # https://github.com/qutebrowser/qutebrowser/issues/2071 + 'QXcbWindow: Unhandled client message: ""', + # https://codereview.qt-project.org/176831 + "QObject::disconnect: Unexpected null parameter", + # https://bugreports.qt.io/browse/QTBUG-76391 + "Attribute Qt::AA_ShareOpenGLContexts must be set before " + "QCoreApplication is created.", + # Qt 6.4 beta 1: https://bugreports.qt.io/browse/QTBUG-104741 + "GL format 0 is not supported", + ] + # not using utils.is_mac here, because we can't be sure we can successfully + # import the utils module here. + if sys.platform == 'darwin': + suppressed_msgs += [ + # https://bugreports.qt.io/browse/QTBUG-47154 + ('virtual void QSslSocketBackendPrivate::transmit() SSLRead ' + 'failed with: -9805'), + ] + + if not msg: + msg = "Logged empty message!" + + if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): + level = logging.DEBUG + elif context.category == "qt.webenginecontext" and ( + msg.strip().startswith("GL Type: ") or # Qt 6.3 + msg.strip().startswith("GLImplementation:") # Qt 6.2 + ): + level = logging.DEBUG + else: + level = qt_to_logging[msg_type] + + if context.line is None: + lineno = -1 # type: ignore[unreachable] + else: + lineno = context.line + + if context.function is None: + func = 'none' # type: ignore[unreachable] + elif ':' in context.function: + func = '"{}"'.format(context.function) + else: + func = context.function + + if context.category is None or context.category == 'default': + name = 'qt' + else: + name = 'qt-' + context.category + if msg.splitlines()[0] == ('This application failed to start because it ' + 'could not find or load the Qt platform plugin ' + '"xcb".'): + # Handle this message specially. + msg += ("\n\nOn Archlinux, this should fix the problem:\n" + " pacman -S libxkbcommon-x11") + faulthandler.disable() + + assert _args is not None + if _args.debug: + stack: Optional[str] = ''.join(traceback.format_stack()) + else: + stack = None + + record = log.qt.makeRecord(name=name, level=level, fn=context.file, lno=lineno, + msg=msg, args=(), exc_info=None, func=func, + sinfo=stack) + log.qt.handle(record) + + +class QtWarningFilter(logging.Filter): + + """Filter to filter Qt warnings. + + Attributes: + _pattern: The start of the message. + """ + + def __init__(self, pattern: str) -> None: + super().__init__() + self._pattern = pattern + + def filter(self, record: logging.LogRecord) -> bool: + """Determine if the specified record is to be logged.""" + do_log = not record.msg.strip().startswith(self._pattern) + return do_log + + +@contextlib.contextmanager +def hide_qt_warning(pattern: str, logger: str = 'qt') -> Iterator[None]: + """Hide Qt warnings matching the given regex.""" + log_filter = QtWarningFilter(pattern) + logger_obj = logging.getLogger(logger) + logger_obj.addFilter(log_filter) + try: + yield + finally: + logger_obj.removeFilter(log_filter) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index cc34057ef..beebcc5c2 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -32,7 +32,7 @@ import pathlib import operator import contextlib from typing import (Any, AnyStr, TYPE_CHECKING, BinaryIO, IO, Iterator, - Optional, Union, Tuple, Protocol, cast) + Optional, Union, Tuple, Protocol, cast, TypeVar) from qutebrowser.qt import machinery, sip from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray, @@ -46,6 +46,7 @@ except ImportError: # pragma: no cover if TYPE_CHECKING: from qutebrowser.qt.webkit import QWebHistory from qutebrowser.qt.webenginecore import QWebEngineHistory + from typing_extensions import TypeGuard # added in Python 3.10 from qutebrowser.misc import objects from qutebrowser.utils import usertypes, utils @@ -102,7 +103,11 @@ def version_check(version: str, parsed = utils.VersionNumber.parse(version) op = operator.eq if exact else operator.ge - result = op(utils.VersionNumber.parse(qVersion()), parsed) + + qversion = qVersion() + assert qversion is not None + result = op(utils.VersionNumber.parse(qversion), parsed) + if compiled and result: # qVersion() ==/>= parsed, now check if QT_VERSION_STR ==/>= parsed. result = op(utils.VersionNumber.parse(QT_VERSION_STR), parsed) @@ -535,24 +540,58 @@ def interpolate_color( if colorspace is None: if percent == 100: - return QColor(*end.getRgb()) + r, g, b, a = end.getRgb() + assert r is not None + assert g is not None + assert b is not None + assert a is not None + return QColor(r, g, b, a) else: - return QColor(*start.getRgb()) + r, g, b, a = start.getRgb() + assert r is not None + assert g is not None + assert b is not None + assert a is not None + return QColor(r, g, b, a) out = QColor() if colorspace == QColor.Spec.Rgb: r1, g1, b1, a1 = start.getRgb() r2, g2, b2, a2 = end.getRgb() + assert r1 is not None + assert g1 is not None + assert b1 is not None + assert a1 is not None + assert r2 is not None + assert g2 is not None + assert b2 is not None + assert a2 is not None components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent) out.setRgb(*components) elif colorspace == QColor.Spec.Hsv: h1, s1, v1, a1 = start.getHsv() h2, s2, v2, a2 = end.getHsv() + assert h1 is not None + assert s1 is not None + assert v1 is not None + assert a1 is not None + assert h2 is not None + assert s2 is not None + assert v2 is not None + assert a2 is not None components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent) out.setHsv(*components) elif colorspace == QColor.Spec.Hsl: h1, s1, l1, a1 = start.getHsl() h2, s2, l2, a2 = end.getHsl() + assert h1 is not None + assert s1 is not None + assert l1 is not None + assert a1 is not None + assert h2 is not None + assert s2 is not None + assert l2 is not None + assert a2 is not None components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent) out.setHsl(*components) else: @@ -611,3 +650,31 @@ def extract_enum_val(val: Union[sip.simplewrapper, int, enum.Enum]) -> int: elif isinstance(val, sip.simplewrapper): return int(val) # type: ignore[call-overload] return val + + +_T = TypeVar("_T") + + +if machinery.IS_QT5: + # On Qt 5, add/remove Optional where type annotations don't have it. + # Also we have a special QT_NONE, which (being Any) we can pass to functions + # where PyQt type hints claim that it's not allowed. + + def remove_optional(obj: Optional[_T]) -> _T: + return cast(_T, obj) + + def add_optional(obj: _T) -> Optional[_T]: + return cast(Optional[_T], obj) + + QT_NONE: Any = None +else: + # On Qt 6, all those things are handled correctly by type annotations, so we + # have a no-op below. + + def remove_optional(obj: Optional[_T]) -> Optional[_T]: + return obj + + def add_optional(obj: Optional[_T]) -> Optional[_T]: + return obj + + QT_NONE = None diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 884a26376..a1fa414f7 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -28,7 +28,7 @@ from typing import Iterator, Optional from qutebrowser.qt.core import QStandardPaths from qutebrowser.qt.widgets import QApplication -from qutebrowser.utils import log, debug, utils, version +from qutebrowser.utils import log, debug, utils, version, qtutils # The cached locations _locations = {} @@ -65,7 +65,7 @@ def _unset_organization() -> Iterator[None]: qapp = QApplication.instance() if qapp is not None: orgname = qapp.organizationName() - qapp.setOrganizationName(None) # type: ignore[arg-type] + qapp.setOrganizationName(qtutils.QT_NONE) try: yield finally: diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index e00c9dab2..1bb035939 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -179,9 +179,9 @@ def _get_search_url(txt: str) -> QUrl: url = QUrl.fromUserInput(evaluated) else: url = QUrl.fromUserInput(config.val.url.searchengines[engine]) - url.setPath(None) # type: ignore[arg-type] - url.setFragment(None) # type: ignore[arg-type] - url.setQuery(None) # type: ignore[call-overload] + url.setPath(qtutils.QT_NONE) + url.setFragment(qtutils.QT_NONE) + url.setQuery(qtutils.QT_NONE) qtutils.ensure_valid(url) return url diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index a81952b7d..dd3cf6ac3 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -128,17 +128,20 @@ class VersionNumber: return NotImplemented return self._ver != other._ver + # FIXME:mypy type ignores below needed for PyQt5-stubs: + # Unsupported left operand type for ... ("QVersionNumber") + def __ge__(self, other: 'VersionNumber') -> bool: - return self._ver >= other._ver # type: ignore[operator] + return self._ver >= other._ver # type: ignore[operator,unused-ignore] def __gt__(self, other: 'VersionNumber') -> bool: - return self._ver > other._ver # type: ignore[operator] + return self._ver > other._ver # type: ignore[operator,unused-ignore] def __le__(self, other: 'VersionNumber') -> bool: - return self._ver <= other._ver # type: ignore[operator] + return self._ver <= other._ver # type: ignore[operator,unused-ignore] def __lt__(self, other: 'VersionNumber') -> bool: - return self._ver < other._ver # type: ignore[operator] + return self._ver < other._ver # type: ignore[operator,unused-ignore] class Unreachable(Exception): @@ -516,6 +519,13 @@ def sanitize_filename(name: str, return name +def _clipboard() -> QClipboard: + """Get the QClipboard and make sure it's not None.""" + clipboard = QApplication.clipboard() + assert clipboard is not None + return clipboard + + def set_clipboard(data: str, selection: bool = False) -> None: """Set the clipboard to some given data.""" global fake_clipboard @@ -527,7 +537,7 @@ def set_clipboard(data: str, selection: bool = False) -> None: fake_clipboard = data else: mode = QClipboard.Mode.Selection if selection else QClipboard.Mode.Clipboard - QApplication.clipboard().setText(data, mode=mode) + _clipboard().setText(data, mode=mode) def get_clipboard(selection: bool = False, fallback: bool = False) -> str: @@ -553,7 +563,7 @@ def get_clipboard(selection: bool = False, fallback: bool = False) -> str: fake_clipboard = None else: mode = QClipboard.Mode.Selection if selection else QClipboard.Mode.Clipboard - data = QApplication.clipboard().text(mode=mode) + data = _clipboard().text(mode=mode) target = "Primary selection" if selection else "Clipboard" if not data.strip(): @@ -565,7 +575,7 @@ def get_clipboard(selection: bool = False, fallback: bool = False) -> str: def supports_selection() -> bool: """Check if the OS supports primary selection.""" - return QApplication.clipboard().supportsSelection() + return _clipboard().supportsSelection() def open_file(filename: str, cmdline: str = None) -> None: diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 782261745..43d6e4d06 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -32,7 +32,7 @@ import getpass import functools import dataclasses import importlib.metadata -from typing import (Mapping, Optional, Sequence, Tuple, ClassVar, Dict, cast, Any, +from typing import (Mapping, Optional, Sequence, Tuple, ClassVar, Dict, Any, TYPE_CHECKING) from qutebrowser.qt import machinery @@ -886,7 +886,10 @@ def version_info() -> str: if objects.qapp: style = objects.qapp.style() - lines.append('Style: {}'.format(style.metaObject().className())) + assert style is not None + metaobj = style.metaObject() + assert metaobj is not None + lines.append('Style: {}'.format(metaobj.className())) lines.append('Platform plugin: {}'.format(objects.qapp.platformName())) lines.append('OpenGL: {}'.format(opengl_info())) @@ -1005,7 +1008,7 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover vendor, version = override.split(', ', maxsplit=1) return OpenGLInfo.parse(vendor=vendor, version=version) - old_context = cast(Optional[QOpenGLContext], QOpenGLContext.currentContext()) + old_context: Optional[QOpenGLContext] = QOpenGLContext.currentContext() old_surface = None if old_context is None else old_context.surface() surface = QOffscreenSurface() diff --git a/requirements.txt b/requirements.txt index b0c1444f8..b5bab3296 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,11 +2,12 @@ adblock==0.6.0 colorama==0.4.6 -importlib-resources==5.12.0 ; python_version=="3.8.*" +importlib-resources==6.0.0 ; python_version=="3.8.*" Jinja2==3.1.2 MarkupSafe==2.1.3 Pygments==2.15.1 -PyYAML==6.0 +PyYAML==6.0.1 +zipp==3.16.2 # Unpinned due to recompile_requirements.py limitations pyobjc-core ; sys_platform=="darwin" pyobjc-framework-Cocoa ; sys_platform=="darwin" diff --git a/scripts/dev/changelog_urls.json b/scripts/dev/changelog_urls.json index baf9e2583..89d3b332a 100644 --- a/scripts/dev/changelog_urls.json +++ b/scripts/dev/changelog_urls.json @@ -104,7 +104,6 @@ "PyQt-builder": "https://www.riverbankcomputing.com/news", "PyQt5-sip": "https://www.riverbankcomputing.com/news", "PyQt5-stubs": "https://github.com/python-qt-tools/PyQt5-stubs/blob/master/CHANGELOG.md", - "PyQt6-stubs": "https://github.com/python-qt-tools/PyQt6-stubs/commits/main", "sip": "https://www.riverbankcomputing.com/news", "PyQt6": "https://www.riverbankcomputing.com/news", "PyQt6-Qt6": "https://www.riverbankcomputing.com/news", @@ -138,8 +137,8 @@ "pyroma": "https://github.com/regebro/pyroma/blob/master/CHANGES.txt", "adblock": "https://github.com/ArniDagur/python-adblock/blob/master/CHANGELOG.md", "importlib-resources": "https://importlib-resources.readthedocs.io/en/latest/history.html", - "importlib-metadata": "https://github.com/python/importlib_metadata/blob/main/CHANGES.rst", - "zipp": "https://github.com/jaraco/zipp/blob/main/CHANGES.rst", + "importlib-metadata": "https://github.com/python/importlib_metadata/blob/main/NEWS.rst", + "zipp": "https://github.com/jaraco/zipp/blob/main/NEWS.rst", "pip": "https://pip.pypa.io/en/stable/news/", "wheel": "https://wheel.readthedocs.io/en/stable/news.html", "setuptools": "https://setuptools.readthedocs.io/en/latest/history.html", @@ -148,7 +147,7 @@ "bleach": "https://github.com/mozilla/bleach/blob/main/CHANGES", "jeepney": "https://gitlab.com/takluyver/jeepney/-/blob/master/docs/release-notes.rst", "keyring": "https://github.com/jaraco/keyring/blob/main/NEWS.rst", - "jaraco.classes": "https://github.com/jaraco/jaraco.classes/blob/main/CHANGES.rst", + "jaraco.classes": "https://github.com/jaraco/jaraco.classes/blob/main/NEWS.rst", "pkginfo": "https://bazaar.launchpad.net/~tseaver/pkginfo/trunk/view/head:/CHANGES.txt", "readme-renderer": "https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst", "requests-toolbelt": "https://github.com/requests/toolbelt/blob/master/HISTORY.rst", @@ -165,5 +164,6 @@ "pyproject_hooks": "https://pyproject-hooks.readthedocs.io/en/latest/changelog.html", "markdown-it-py": "https://github.com/executablebooks/markdown-it-py/blob/master/CHANGELOG.md", "mdurl": "https://github.com/executablebooks/mdurl/commits/master", - "blinker": "https://blinker.readthedocs.io/en/stable/#changes" + "blinker": "https://blinker.readthedocs.io/en/stable/#changes", + "exceptiongroup": "https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst" } diff --git a/scripts/dev/ci/docker/Dockerfile.j2 b/scripts/dev/ci/docker/Dockerfile.j2 index e9a0facd7..3a1adbdef 100644 --- a/scripts/dev/ci/docker/Dockerfile.j2 +++ b/scripts/dev/ci/docker/Dockerfile.j2 @@ -38,7 +38,10 @@ RUN pacman -Su --noconfirm \ RUN pacman -U --noconfirm \ https://archive.archlinux.org/packages/q/qt5-webkit/qt5-webkit-5.212.0alpha4-18-x86_64.pkg.tar.zst \ https://archive.archlinux.org/packages/p/python-pyqt5/python-pyqt5-5.15.7-2-x86_64.pkg.tar.zst \ - https://archive.archlinux.org/packages/p/python/python-3.10.10-1-x86_64.pkg.tar.zst + https://archive.archlinux.org/packages/p/python/python-3.10.10-1-x86_64.pkg.tar.zst \ + https://archive.archlinux.org/packages/i/icu/icu-72.1-2-x86_64.pkg.tar.zst \ + https://archive.archlinux.org/packages/l/libxml2/libxml2-2.10.4-4-x86_64.pkg.tar.zst \ + https://archive.archlinux.org/packages/q/qt5-base/qt5-base-5.15.10%2Bkde%2Br129-3-x86_64.pkg.tar.zst RUN python3 -m ensurepip RUN python3 -m pip install tox pyqt5-sip {% endif %} diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 62b409a19..aad8f2792 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -147,6 +147,14 @@ Feature: Searching on a page And I run :search-next Then the error "No search done yet." should be shown + # https://github.com/qutebrowser/qutebrowser/issues/7275 + @qtwebkit_skip + Scenario: Jumping to next without matches + When I run :search doesnotmatch + And I wait for the warning "Text 'doesnotmatch' not found on page!" + And I run :search-next + Then the warning "Text 'doesnotmatch' not found on page!" should be shown + Scenario: Repeating search in a second tab (issue #940) When I open data/search.html in a new tab And I run :search foo @@ -222,6 +230,14 @@ Feature: Searching on a page And I run :search-prev Then the error "No search done yet." should be shown + # https://github.com/qutebrowser/qutebrowser/issues/7275 + @qtwebkit_skip + Scenario: Jumping to previous without matches + When I run :search doesnotmatch + And I wait for the warning "Text 'doesnotmatch' not found on page!" + And I run :search-prev + Then the warning "Text 'doesnotmatch' not found on page!" should be shown + ## wrapping Scenario: Wrapping around page diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 56524a031..caa86dfbb 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -25,6 +25,7 @@ import importlib import re import json import platform +from contextlib import nullcontext as does_not_raise import pytest from qutebrowser.qt.core import QProcess, QPoint @@ -916,3 +917,15 @@ def test_sandboxing( status = dict(line.split("\t") for line in lines) assert status == expected_status + + +@pytest.mark.not_frozen +def test_logfilter_arg_does_not_crash(request, quteproc_new): + args = ['--temp-basedir', '--debug', '--logfilter', 'commands, init, ipc, webview'] + + with does_not_raise(): + quteproc_new.start(args=args + _base_args(request.config)) + + # Waiting for quit to make sure no other warning is emitted + quteproc_new.send_cmd(':quit') + quteproc_new.wait_for_quit() diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 51b014f81..6eb1c4e4f 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -22,11 +22,9 @@ import argparse import itertools import sys import warnings -import dataclasses import pytest import _pytest.logging # pylint: disable=import-private-name -from qutebrowser.qt import core as qtcore from qutebrowser import qutebrowser from qutebrowser.utils import log @@ -241,7 +239,7 @@ class TestInitLog: @pytest.fixture(autouse=True) def setup(self, mocker): - mocker.patch('qutebrowser.utils.log.qtcore.qInstallMessageHandler', + mocker.patch('qutebrowser.utils.qtlog.qtcore.qInstallMessageHandler', autospec=True) yield # Make sure logging is in a sensible default state @@ -342,35 +340,6 @@ class TestInitLog: assert log.console_filter.names == {'misc'} -class TestHideQtWarning: - - """Tests for hide_qt_warning/QtWarningFilter.""" - - @pytest.fixture - def qt_logger(self): - return logging.getLogger('qt-tests') - - def test_unfiltered(self, qt_logger, caplog): - with log.hide_qt_warning("World", 'qt-tests'): - with caplog.at_level(logging.WARNING, 'qt-tests'): - qt_logger.warning("Hello World") - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'WARNING' - assert record.message == "Hello World" - - @pytest.mark.parametrize('line', [ - "Hello", # exact match - "Hello World", # match at start of line - " Hello World ", # match with spaces - ]) - def test_filtered(self, qt_logger, caplog, line): - with log.hide_qt_warning("Hello", 'qt-tests'): - with caplog.at_level(logging.WARNING, 'qt-tests'): - qt_logger.warning(line) - assert not caplog.records - - @pytest.mark.parametrize('suffix, expected', [ ('', 'STUB: test_stub'), ('foo', 'STUB: test_stub (foo)'), @@ -405,27 +374,3 @@ def test_warning_still_errors(): # Mainly a sanity check after the tests messing with warnings above. with pytest.raises(UserWarning): warnings.warn("error", UserWarning) - - -class TestQtMessageHandler: - - @dataclasses.dataclass - class Context: - - """Fake QMessageLogContext.""" - - function: str = None - category: str = None - file: str = None - line: int = None - - @pytest.fixture(autouse=True) - def init_args(self): - parser = qutebrowser.get_argparser() - args = parser.parse_args([]) - log.init_log(args) - - def test_empty_message(self, caplog): - """Make sure there's no crash with an empty message.""" - log.qt_message_handler(qtcore.QtMsgType.QtDebugMsg, self.Context(), "") - assert caplog.messages == ["Logged empty message!"] diff --git a/tests/unit/utils/test_qtlog.py b/tests/unit/utils/test_qtlog.py new file mode 100644 index 000000000..3dd62b9a9 --- /dev/null +++ b/tests/unit/utils/test_qtlog.py @@ -0,0 +1,82 @@ +# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. + + +"""Tests for qutebrowser.utils.qtlog.""" + +import dataclasses +import logging + +import pytest + +from qutebrowser import qutebrowser +from qutebrowser.utils import qtlog + +from qutebrowser.qt import core as qtcore + + +class TestQtMessageHandler: + + @dataclasses.dataclass + class Context: + + """Fake QMessageLogContext.""" + + function: str = None + category: str = None + file: str = None + line: int = None + + @pytest.fixture(autouse=True) + def init_args(self): + parser = qutebrowser.get_argparser() + args = parser.parse_args([]) + qtlog.init(args) + + def test_empty_message(self, caplog): + """Make sure there's no crash with an empty message.""" + qtlog.qt_message_handler(qtcore.QtMsgType.QtDebugMsg, self.Context(), "") + assert caplog.messages == ["Logged empty message!"] + + +class TestHideQtWarning: + + """Tests for hide_qt_warning/QtWarningFilter.""" + + @pytest.fixture + def qt_logger(self): + return logging.getLogger('qt-tests') + + def test_unfiltered(self, qt_logger, caplog): + with qtlog.hide_qt_warning("World", 'qt-tests'): + with caplog.at_level(logging.WARNING, 'qt-tests'): + qt_logger.warning("Hello World") + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'WARNING' + assert record.message == "Hello World" + + @pytest.mark.parametrize('line', [ + "Hello", # exact match + "Hello World", # match at start of line + " Hello World ", # match with spaces + ]) + def test_filtered(self, qt_logger, caplog, line): + with qtlog.hide_qt_warning("Hello", 'qt-tests'): + with caplog.at_level(logging.WARNING, 'qt-tests'): + qt_logger.warning(line) + assert not caplog.records @@ -16,7 +16,7 @@ setenv = pyqt{515,5152}: PYTEST_QT_API=pyqt5 pyqt{515,5152}: QUTE_QT_WRAPPER=PyQt5 cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= - py312: VIRTUALENV_PIP=23.1.2 + py312: VIRTUALENV_PIP=23.2 py312: PIP_REQUIRE_VIRTUALENV=0 passenv = PYTHON @@ -96,7 +96,9 @@ basepython = {env:PYTHON:python3} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-vulture.txt -setenv = PYTHONPATH={toxinidir} +setenv = + {[testenv]setenv} + {[testenv:vulture]setenv} commands = {envpython} scripts/link_pyqt.py --tox {envdir} {[testenv:vulture]commands} @@ -224,6 +226,7 @@ deps = -r{toxinidir}/misc/requirements/requirements-tests.txt -r{toxinidir}/misc/requirements/requirements-mypy.txt pyqt6: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt +commands_pre = pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt6 commands = {envpython} -m mypy {env:QUTE_CONSTANTS_ARGS} qutebrowser {posargs} |