summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml3
-rw-r--r--.mypy.ini268
-rw-r--r--doc/changelog.asciidoc10
-rw-r--r--doc/help/settings.asciidoc25
-rw-r--r--misc/requirements/requirements-mypy.txt5
-rw-r--r--misc/requirements/requirements-mypy.txt-raw3
-rw-r--r--pyrightconfig.json11
-rw-r--r--qutebrowser/app.py6
-rw-r--r--qutebrowser/browser/browsertab.py31
-rw-r--r--qutebrowser/browser/commands.py7
-rw-r--r--qutebrowser/browser/hints.py18
-rw-r--r--qutebrowser/browser/network/pac.py18
-rw-r--r--qutebrowser/browser/pdfjs.py7
-rw-r--r--qutebrowser/browser/shared.py8
-rw-r--r--qutebrowser/browser/webengine/interceptor.py6
-rw-r--r--qutebrowser/browser/webengine/notification.py25
-rw-r--r--qutebrowser/browser/webengine/webenginequtescheme.py13
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py19
-rw-r--r--qutebrowser/browser/webkit/webview.py7
-rw-r--r--qutebrowser/config/configdata.yml24
-rw-r--r--qutebrowser/config/qtargs.py9
-rw-r--r--qutebrowser/config/websettings.py2
-rw-r--r--qutebrowser/keyinput/eventfilter.py15
-rw-r--r--qutebrowser/keyinput/keyutils.py14
-rw-r--r--qutebrowser/keyinput/modeman.py23
-rw-r--r--qutebrowser/mainwindow/mainwindow.py11
-rw-r--r--qutebrowser/mainwindow/prompt.py2
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py2
-rw-r--r--qutebrowser/mainwindow/statusbar/command.py10
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py3
-rw-r--r--qutebrowser/misc/crashdialog.py3
-rw-r--r--qutebrowser/misc/earlyinit.py7
-rw-r--r--qutebrowser/misc/httpclient.py4
-rw-r--r--qutebrowser/misc/ipc.py7
-rw-r--r--qutebrowser/misc/miscwidgets.py4
-rw-r--r--qutebrowser/misc/nativeeventfilter.py16
-rw-r--r--qutebrowser/misc/sql.py2
-rw-r--r--qutebrowser/qt/_core_pyqtproperty.py78
-rw-r--r--qutebrowser/qt/core.py4
-rw-r--r--qutebrowser/qt/sip.py2
-rw-r--r--qutebrowser/qt/webkit.py8
-rw-r--r--qutebrowser/qt/webkitwidgets.py9
-rw-r--r--qutebrowser/qutebrowser.py6
-rw-r--r--qutebrowser/utils/debug.py23
-rw-r--r--qutebrowser/utils/jinja.py2
-rw-r--r--qutebrowser/utils/log.py7
-rw-r--r--qutebrowser/utils/qtutils.py13
-rw-r--r--qutebrowser/utils/urlutils.py61
-rw-r--r--qutebrowser/utils/version.py6
-rw-r--r--scripts/dev/changelog_urls.json5
-rw-r--r--scripts/dev/misc_checks.py4
-rwxr-xr-xscripts/dev/run_vulture.py5
-rw-r--r--tests/unit/config/test_qtargs.py15
53 files changed, 657 insertions, 239 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d64f08de9..64dddd2f8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,8 +21,7 @@ jobs:
include:
- testenv: pylint
- testenv: flake8
- # FIXME:qt6 (lint)
- # - testenv: mypy-pyqt6
+ - testenv: mypy-pyqt6
- testenv: mypy-pyqt5
- testenv: docs
- testenv: vulture
diff --git a/.mypy.ini b/.mypy.ini
index 03b5a0b1d..ef34dbada 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -6,7 +6,7 @@ warn_unused_configs = True
disallow_any_generics = True
disallow_subclassing_any = True
# disallow_untyped_calls = True
-# disallow_untyped_defs = True
+disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
@@ -29,18 +29,10 @@ pretty = True
### FIXME:v4 get rid of this
no_implicit_optional = False
-[mypy-colorama]
-# https://github.com/tartley/colorama/issues/206
-ignore_missing_imports = True
-
[mypy-hunter]
# https://github.com/ionelmc/python-hunter/issues/43
ignore_missing_imports = True
-[mypy-pygments.*]
-# https://github.com/pygments/pygments/issues/1189
-ignore_missing_imports = True
-
[mypy-objc]
# https://github.com/ronaldoussoren/pyobjc/issues/417
ignore_missing_imports = True
@@ -49,90 +41,230 @@ ignore_missing_imports = True
# https://github.com/ronaldoussoren/pyobjc/issues/417
ignore_missing_imports = True
-[mypy-qutebrowser.browser.browsertab]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webkit.*]
+ignore_errors = True
-[mypy-qutebrowser.browser.hints]
-disallow_untyped_defs = True
+[mypy-qutebrowser.config.configtypes]
+# Needs some major work to use specific generics
+disallow_any_generics = False
-[mypy-qutebrowser.browser.inspector]
-disallow_untyped_defs = True
+# Modules that are not fully typed yet
+[mypy-qutebrowser.app]
+disallow_untyped_defs = False
-[mypy-qutebrowser.browser.webkit.webkitinspector]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.commands]
+disallow_untyped_defs = False
-[mypy-qutebrowser.browser.webengine.webengineinspector]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.downloads]
+disallow_untyped_defs = False
-[mypy-qutebrowser.browser.webengine.notification]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.downloadview]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.guiprocess]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.eventfilter]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.objects]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.greasemonkey]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.quitter]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.history]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.debugcachestats]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.navigate]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.elf]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.network.pac]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.utilcmds]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.network.proxy]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.throttle]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.pdfjs]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.backendproblem]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.qtnetworkdownloads]
+disallow_untyped_defs = False
-[mypy-qutebrowser.config.*]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.shared]
+disallow_untyped_defs = False
-[mypy-qutebrowser.config.configtypes]
-# Needs some major work to use specific generics
-disallow_any_generics = False
+[mypy-qutebrowser.browser.signalfilter]
+disallow_untyped_defs = False
-[mypy-qutebrowser.api.*]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.urlmarks]
+disallow_untyped_defs = False
-[mypy-qutebrowser.components.*]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.cookies]
+disallow_untyped_defs = False
-[mypy-qutebrowser.extensions.*]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.interceptor]
+disallow_untyped_defs = False
-[mypy-qutebrowser.browser.webelem]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.spell]
+disallow_untyped_defs = False
-[mypy-qutebrowser.browser.webkit.webkitelem]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.tabhistory]
+disallow_untyped_defs = False
-[mypy-qutebrowser.browser.webengine.webengineelem]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.webenginedownloads]
+disallow_untyped_defs = False
-[mypy-qutebrowser.browser.webengine.darkmode]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.webenginequtescheme]
+disallow_untyped_defs = False
-[mypy-qutebrowser.keyinput.*]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.webenginesettings]
+disallow_untyped_defs = False
-[mypy-qutebrowser.utils.*]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.webenginetab]
+disallow_untyped_defs = False
-[mypy-qutebrowser.mainwindow.statusbar.command]
-disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.webview]
+disallow_untyped_defs = False
-[mypy-qutebrowser.browser.qutescheme]
-disallow_untyped_defs = True
+[mypy-qutebrowser.commands.argparser]
+disallow_untyped_defs = False
-[mypy-qutebrowser.completion.models.filepathcategory]
-disallow_untyped_defs = True
+[mypy-qutebrowser.commands.cmdexc]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.commands.command]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.commands.runners]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.commands.userscripts]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.completer]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.completiondelegate]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.completionwidget]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.models.completionmodel]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.models.configmodel]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.models.histcategory]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.models.listcategory]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.models.miscmodels]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.models.urlmodel]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.completion.models.util]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.mainwindow]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.messageview]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.prompt]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.backforward]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.bar]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.clock]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.keystring]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.percentage]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.progress]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.tabindex]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.textbase]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.statusbar.url]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.tabbedbrowser]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.tabwidget]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.mainwindow.windowundo]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.autoupdate]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.checkpyver]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.cmdhistory]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.consolewidget]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.crashdialog]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.crashsignal]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.earlyinit]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.editor]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.httpclient]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.ipc]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.keyhintwidget]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.lineparser]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.miscwidgets]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.msgbox]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.pastebin]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.savemanager]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.sessions]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.misc.split]
+disallow_untyped_defs = False
+
+[mypy-qutebrowser.qutebrowser]
+disallow_untyped_defs = False
-[mypy-qutebrowser.misc.nativeeventfilter]
-disallow_untyped_defs = True
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 20d940f0e..71cf6ec5d 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -50,6 +50,9 @@ Added
* `qutedmenu` gained new `window` and `private` options.
* `qute-keepassxc` now supports unlock-on-demand, multiple account
selection via rofi, and inserting TOTP-codes (experimental).
+- New `qt.chromium.experimental_web_platform_features` setting, which is enabled
+ on Qt 5 by default, to maximize compatibility with websites despite an aging
+ Chromium backend.
- New `colors.webpage.darkmode.increase_text_contrast` setting for Qt 6.3+
- New `fonts.tooltip`, `colors.tooltip.bg` and `colors.tooltip.fg` settings.
- New `log-qt-events` debug flag for `-D`
@@ -64,6 +67,9 @@ Removed
dropped, as older Qt versions are
https://endoflife.date/qt[end-of-life upstream] since mid/late 2020
(5.13/5.14) and late 2021 (5.12 LTS).
+- The `--enable-webengine-inspector` flag is now dropped. It used to be ignored
+ but still accepted, to allow doing a `:restart` from versions older than v2.0.0.
+ Thus, switching from v1.x.x directly to v3.0.0 via `:restart` will not be possible.
- It's planned to drop support for various legacy platforms and libraries which
are unsupported upstream, such as:
* The QtWebKit backend
@@ -148,6 +154,10 @@ Changed
- The qute-pass will now try looking up candidate pass entries based on the
calling tab's verbatim netloc (hostname including port and username) if it
can't find a match with an earlier candidate (FQDN, IPv4 etc).
+- The `js-string-replaceall` quirk is now removed from the default
+ `content.site_specific_quirks.skip`, so that `String.replaceAll` is now
+ polyfilled on QtWebEngine < 5.15.3, hopefully improving website
+ compaitibility.
Fixed
~~~~~
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 34262b945..1b28eb39f 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -294,6 +294,7 @@
|<<prompt.filebrowser,prompt.filebrowser>>|Show a filebrowser in download prompts.
|<<prompt.radius,prompt.radius>>|Rounding radius (in pixels) for the edges of prompts.
|<<qt.args,qt.args>>|Additional arguments to pass to Qt, without leading `--`.
+|<<qt.chromium.experimental_web_platform_features,qt.chromium.experimental_web_platform_features>>|Enables Web Platform features that are in development.
|<<qt.chromium.low_end_device_mode,qt.chromium.low_end_device_mode>>|When to use Chromium's low-end device mode.
|<<qt.chromium.process_model,qt.chromium.process_model>>|Which Chromium process model to use.
|<<qt.chromium.sandboxing,qt.chromium.sandboxing>>|What sandboxing mechanisms in Chromium to use.
@@ -2787,7 +2788,6 @@ Default: +pass:[true]+
[[content.site_specific_quirks.skip]]
=== content.site_specific_quirks.skip
Disable a list of named quirks.
-The js-string-replaceall quirk is needed for Nextcloud Calendar < 2.2.0 with QtWebEngine < 5.15.3. However, the workaround is not fully compliant to the ECMAScript spec and might cause issues on other websites, so it's disabled by default.
Type: <<types,FlagList>>
@@ -2804,9 +2804,7 @@ Valid values:
* +misc-krunker+
* +misc-mathml-darkmode+
-Default:
-
-- +pass:[js-string-replaceall]+
+Default: empty
[[content.tls.certificate_errors]]
=== content.tls.certificate_errors
@@ -3851,6 +3849,25 @@ Type: <<types,List of String>>
Default: empty
+[[qt.chromium.experimental_web_platform_features]]
+=== qt.chromium.experimental_web_platform_features
+Enables Web Platform features that are in development.
+This passes the `--enable-experimental-web-platform-features` flag to Chromium. By default, this is enabled with Qt 5 to maximize compatibility despite an aging Chromium base.
+
+This setting requires a restart.
+
+This setting is only available with the QtWebEngine backend.
+
+Type: <<types,String>>
+
+Valid values:
+
+ * +always+: Enable experimental web platform features.
+ * +auto+: Enable experimental web platform features when using Qt 5.
+ * +never+: Disable experimental web platform features.
+
+Default: +pass:[auto]+
+
[[qt.chromium.low_end_device_mode]]
=== qt.chromium.low_end_device_mode
When to use Chromium's low-end device mode.
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index 4b512d579..b5d23bf61 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -13,6 +13,11 @@ 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
diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw
index 53a2e82f3..487d30ca6 100644
--- a/misc/requirements/requirements-mypy.txt-raw
+++ b/misc/requirements/requirements-mypy.txt-raw
@@ -3,7 +3,10 @@ lxml # For HTML reports
diff-cover
PyQt5-stubs
+git+https://github.com/python-qt-tools/PyQt6-stubs.git
types-PyYAML
+types-colorama
+types-Pygments
# So stubs are available even on newer Python versions
importlib_resources
diff --git a/pyrightconfig.json b/pyrightconfig.json
index 138687c53..6371938c8 100644
--- a/pyrightconfig.json
+++ b/pyrightconfig.json
@@ -1,10 +1,11 @@
{
"defineConstant": {
- "USE_PYQT6": false,
- "USE_PYQT5": true,
- "USE_PYSIDE2": false,
+ "USE_PYQT6": true,
+ "USE_PYQT5": false,
"USE_PYSIDE6": false,
- "IS_QT5": true,
- "IS_QT6": false
+ "IS_QT5": false,
+ "IS_QT6": true,
+ "IS_PYQT": true,
+ "IS_PYSIDE": false
}
}
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 49488975a..bb2ff56e7 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -561,11 +561,9 @@ class Application(QApplication):
self.launch_time = datetime.datetime.now()
self.focusObjectChanged.connect(self.on_focus_object_changed)
- try:
- self.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)
- except AttributeError:
+ if machinery.IS_QT5:
# default and removed in Qt 6
- pass
+ self.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)
self.new_window.connect(self._on_new_window)
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index a11117e50..47cba5922 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -25,6 +25,7 @@ import dataclasses
from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional,
Sequence, Set, Type, Union, Tuple)
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt,
QEvent, QPoint, QRect)
from qutebrowser.qt.gui import QKeyEvent, QIcon, QPixmap
@@ -35,8 +36,9 @@ from qutebrowser.qt.network import QNetworkAccessManager
if TYPE_CHECKING:
from qutebrowser.qt.webkit import QWebHistory, QWebHistoryItem
from qutebrowser.qt.webkitwidgets import QWebPage, QWebView
- from qutebrowser.qt.webenginewidgets import (
- QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage, QWebEngineView)
+ from qutebrowser.qt.webenginecore import (
+ QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage)
+ from qutebrowser.qt.webenginewidgets import QWebEngineView
from qutebrowser.keyinput import modeman
from qutebrowser.config import config, websettings
@@ -1066,8 +1068,11 @@ class AbstractTab(QWidget):
def _set_widget(self, widget: Union["QWebView", "QWebEngineView"]) -> None:
# pylint: disable=protected-access
self._widget = widget
+ # FIXME:v4 ignore needed for QtWebKit
self.data.splitter = miscwidgets.InspectorSplitter(
- win_id=self.win_id, main_webview=widget)
+ win_id=self.win_id,
+ main_webview=widget, # type: ignore[arg-type,unused-ignore]
+ )
self._layout.wrap(self, self.data.splitter)
self.history._history = widget.history()
self.history.private_api._history = widget.history()
@@ -1174,7 +1179,7 @@ class AbstractTab(QWidget):
@pyqtSlot(bool)
def _on_load_finished(self, ok: bool) -> None:
assert self._widget is not None
- if sip.isdeleted(self._widget):
+ if self.is_deleted():
# https://github.com/qutebrowser/qutebrowser/issues/3498
return
@@ -1309,18 +1314,22 @@ class AbstractTab(QWidget):
pic = self._widget.grab()
else:
qtutils.ensure_valid(rect)
- pic = self._widget.grab(rect)
+ # FIXME:v4 ignore needed for QtWebKit
+ pic = self._widget.grab(rect) # type: ignore[arg-type,unused-ignore]
if pic.isNull():
return None
+ if machinery.IS_QT6:
+ # FIXME:v4 cast needed for QtWebKit
+ pic = cast(QPixmap, pic)
+
return pic
def __repr__(self) -> str:
try:
qurl = self.url()
- as_unicode = QUrl.ComponentFormattingOption.EncodeUnicode
- url = qurl.toDisplayString(as_unicode) # type: ignore[arg-type]
+ url = qurl.toDisplayString(urlutils.FormatOption.ENCODE_UNICODE)
except (AttributeError, RuntimeError) as exc:
url = '<{}>'.format(exc.__class__.__name__)
else:
@@ -1328,5 +1337,11 @@ class AbstractTab(QWidget):
return utils.get_repr(self, tab_id=self.tab_id, url=url)
def is_deleted(self) -> bool:
+ """Check if the tab has been deleted."""
assert self._widget is not None
- return sip.isdeleted(self._widget)
+ # FIXME:v4 cast needed for QtWebKit
+ if machinery.IS_QT6:
+ widget = cast(QWidget, self._widget)
+ else:
+ widget = self._widget
+ return sip.isdeleted(widget)
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 8fd3fc787..3b38c44c0 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -716,9 +716,10 @@ class CommandDispatcher:
assert what in ['url', 'pretty-url'], what
if what == 'pretty-url':
- flags = QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.DecodeReserved
+ flags = urlutils.FormatOption.DECODE_RESERVED
else:
- flags = QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded
+ flags = urlutils.FormatOption.ENCODED
+ flags |= urlutils.FormatOption.REMOVE_PASSWORD
url = QUrl(self._current_url())
url_query = QUrlQuery()
@@ -730,7 +731,7 @@ class CommandDispatcher:
if key in config.val.url.yank_ignored_parameters:
url_query.removeQueryItem(key)
url.setQuery(url_query)
- return url.toString(flags) # type: ignore[arg-type]
+ return url.toString(flags)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 48534c54c..8e4ae7987 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -36,7 +36,7 @@ from qutebrowser.keyinput import modeman, modeparsers, basekeyparser
from qutebrowser.browser import webelem, history
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
-from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
+from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils, urlutils
if TYPE_CHECKING:
from qutebrowser.browser import browsertab
@@ -250,9 +250,9 @@ class HintActions:
sel = (context.target == Target.yank_primary and
utils.supports_selection())
- flags = QUrl.ComponentFormattingOption.FullyEncoded | QUrl.UrlFormattingOption.RemovePassword
+ flags = urlutils.FormatOption.ENCODED | urlutils.FormatOption.REMOVE_PASSWORD
if url.scheme() == 'mailto':
- flags |= QUrl.UrlFormattingOption.RemoveScheme # type: ignore[operator]
+ flags |= urlutils.FormatOption.REMOVE_SCHEME
urlstr = url.toString(flags)
new_content = urlstr
@@ -274,15 +274,14 @@ class HintActions:
def run_cmd(self, url: QUrl, context: HintContext) -> None:
"""Run the command based on a hint URL."""
- urlstr = url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
+ urlstr = url.toString(urlutils.FormatOption.ENCODED)
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely(' '.join(args))
def preset_cmd_text(self, url: QUrl, context: HintContext) -> None:
"""Preset a commandline text based on a hint URL."""
- flags = QUrl.ComponentFormattingOption.FullyEncoded
- urlstr = url.toDisplayString(flags) # type: ignore[arg-type]
+ urlstr = url.toDisplayString(urlutils.FormatOption.ENCODED)
args = context.get_args(urlstr)
text = ' '.join(args)
if text[0] not in modeparsers.STARTCHARS:
@@ -323,19 +322,18 @@ class HintActions:
cmd = context.args[0]
args = context.args[1:]
- flags = QUrl.ComponentFormattingOption.FullyEncoded
+ flags = urlutils.FormatOption.ENCODED
env = {
'QUTE_MODE': 'hints',
'QUTE_SELECTED_TEXT': str(elem),
'QUTE_SELECTED_HTML': elem.outer_xml(),
- 'QUTE_CURRENT_URL':
- context.baseurl.toString(flags), # type: ignore[arg-type]
+ 'QUTE_CURRENT_URL': context.baseurl.toString(flags),
}
url = elem.resolve_url(context.baseurl)
if url is not None:
- env['QUTE_URL'] = url.toString(flags) # type: ignore[arg-type]
+ env['QUTE_URL'] = url.toString(flags)
try:
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py
index b6ed6040f..c66b6bc03 100644
--- a/qutebrowser/browser/network/pac.py
+++ b/qutebrowser/browser/network/pac.py
@@ -19,15 +19,16 @@
import sys
import functools
-from typing import Optional
+from typing import Optional, cast
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import QObject, pyqtSignal, pyqtSlot, QUrl
from qutebrowser.qt.network import (QNetworkProxy, QNetworkRequest, QHostInfo,
QNetworkReply, QNetworkAccessManager,
QHostAddress)
from qutebrowser.qt.qml import QJSEngine, QJSValue
-from qutebrowser.utils import log, utils, qtutils, resources
+from qutebrowser.utils import log, utils, qtutils, resources, urlutils
class ParseProxyError(Exception):
@@ -212,13 +213,20 @@ class PACResolver:
"""
qtutils.ensure_valid(query.url())
+ string_flags: urlutils.UrlFlagsType
if from_file:
string_flags = QUrl.ComponentFormattingOption.PrettyDecoded
else:
- string_flags = QUrl.UrlFormattingOption.RemoveUserInfo # type: ignore[assignment]
+ string_flags = QUrl.UrlFormattingOption.RemoveUserInfo
if query.url().scheme() == 'https':
- string_flags |= QUrl.UrlFormattingOption.RemovePath # type: ignore[assignment]
- string_flags |= QUrl.UrlFormattingOption.RemoveQuery # type: ignore[assignment]
+ https_opts = (
+ QUrl.UrlFormattingOption.RemovePath |
+ QUrl.UrlFormattingOption.RemoveQuery)
+
+ if machinery.IS_QT5:
+ string_flags |= cast(QUrl.UrlFormattingOption, https_opts)
+ else:
+ string_flags |= https_opts
result = self._resolver.call([query.url().toString(string_flags),
query.peerHostName()])
diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py
index 845ff5f9c..fdece9a9e 100644
--- a/qutebrowser/browser/pdfjs.py
+++ b/qutebrowser/browser/pdfjs.py
@@ -22,7 +22,7 @@ import os
from qutebrowser.qt.core import QUrl, QUrlQuery
-from qutebrowser.utils import resources, javascript, jinja, standarddir, log
+from qutebrowser.utils import resources, javascript, jinja, standarddir, log, urlutils
from qutebrowser.config import config
@@ -93,8 +93,7 @@ def _generate_pdfjs_script(filename):
url_query.addQueryItem('filename', filename)
url.setQuery(url_query)
- js_url = javascript.to_js(
- url.toString(QUrl.ComponentFormattingOption.FullyEncoded)) # type: ignore[arg-type]
+ js_url = javascript.to_js(url.toString(urlutils.FormatOption.ENCODED))
return jinja.js_environment.from_string("""
document.addEventListener("DOMContentLoaded", function() {
@@ -243,7 +242,7 @@ def get_main_url(filename: str, original_url: QUrl) -> QUrl:
query = QUrlQuery()
query.addQueryItem('filename', filename) # read from our JS
query.addQueryItem('file', '') # to avoid pdfjs opening the default PDF
- urlstr = original_url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
+ urlstr = original_url.toString(urlutils.FormatOption.ENCODED)
query.addQueryItem('source', urlstr)
url.setQuery(query)
return url
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 567c1f7bc..6e87bc1a5 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -29,7 +29,7 @@ from qutebrowser.qt.core import QUrl, pyqtBoundSignal
from qutebrowser.config import config
from qutebrowser.utils import (usertypes, message, log, objreg, jinja, utils,
- qtutils, version)
+ qtutils, version, urlutils)
from qutebrowser.mainwindow import mainwindow
from qutebrowser.misc import guiprocess, objects
@@ -227,9 +227,7 @@ def handle_certificate_error(
# scheme might not match.
is_resource = (
first_party_url.isValid() and
- not request_url.matches(
- first_party_url,
- QUrl.UrlFormattingOption.RemoveScheme)) # type: ignore[arg-type]
+ not request_url.matches(first_party_url, urlutils.FormatOption.REMOVE_SCHEME))
if conf == 'ask' or conf == 'ask-block-thirdparty' and not is_resource:
err_template = jinja.environment.from_string("""
@@ -259,7 +257,7 @@ def handle_certificate_error(
error=error,
)
urlstr = request_url.toString(
- QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
+ urlutils.FormatOption.REMOVE_PASSWORD | urlutils.FormatOption.ENCODED)
title = "Certificate error"
try:
diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py
index f6feacf5f..75acb3252 100644
--- a/qutebrowser/browser/webengine/interceptor.py
+++ b/qutebrowser/browser/webengine/interceptor.py
@@ -32,7 +32,11 @@ class WebEngineRequest(interceptors.Request):
"""QtWebEngine-specific request interceptor functionality."""
- _WHITELISTED_REQUEST_METHODS = {QByteArray(b'GET'), QByteArray(b'HEAD')}
+ _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]
+ }
def __init__(self, *args, webengine_info, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py
index 9b73de11f..d140a8c61 100644
--- a/qutebrowser/browser/webengine/notification.py
+++ b/qutebrowser/browser/webengine/notification.py
@@ -59,13 +59,12 @@ from qutebrowser.qt.widgets import QSystemTrayIcon
if TYPE_CHECKING:
# putting these behind TYPE_CHECKING also means this module is importable
# on installs that don't have these
- from qutebrowser.qt.webenginecore import QWebEngineNotification
- from qutebrowser.qt.webenginewidgets import QWebEngineProfile
+ from qutebrowser.qt.webenginecore import QWebEngineNotification, QWebEngineProfile
from qutebrowser.config import config
from qutebrowser.misc import objects
from qutebrowser.utils import (
- qtutils, log, utils, debug, message, objreg, resources,
+ qtutils, log, utils, debug, message, objreg, resources, urlutils
)
from qutebrowser.qt import sip
@@ -634,7 +633,7 @@ class HerbeNotificationAdapter(AbstractNotificationAdapter):
def _on_error(self, error: QProcess.ProcessError) -> None:
if error == QProcess.ProcessError.Crashed:
return
- name = debug.qenum_key(QProcess.ProcessError, error)
+ name = debug.qenum_key(QProcess, error)
self.error.emit(f'herbe process error: {name}')
@pyqtSlot(int)
@@ -690,11 +689,12 @@ def _as_uint32(x: int) -> QVariant:
variant = QVariant(x)
if machinery.IS_QT5:
- target_type = QVariant.Type.UInt
+ target = QVariant.Type.UInt
else: # Qt 6
- target_type = QMetaType(QMetaType.Type.UInt.value)
+ # FIXME:mypy PyQt6-stubs issue
+ target = QMetaType(QMetaType.Type.UInt.value) # type: ignore[call-overload]
- successful = variant.convert(target_type)
+ successful = variant.convert(target)
assert successful
return variant
@@ -916,8 +916,8 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
typ = msg.type()
if typ != expected_type:
- type_str = debug.qenum_key(QDBusMessage.MessageType, typ)
- expected_type_str = debug.qenum_key(QDBusMessage.MessageType, expected_type)
+ type_str = debug.qenum_key(QDBusMessage, typ)
+ expected_type_str = debug.qenum_key(QDBusMessage, expected_type)
raise Error(
f"Got a message of type {type_str} but expected {expected_type_str}"
f"(args: {msg.arguments()})")
@@ -1109,7 +1109,8 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
return None
bits = qimage.constBits().asstring(size)
- image_data.add(QByteArray(bits))
+ # FIXME:mypy PyQt6-stubs issue
+ image_data.add(QByteArray(bits)) # type: ignore[call-overload,unused-ignore]
image_data.endStructure()
return image_data
@@ -1176,9 +1177,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
if self._capabilities.kde_origin_name or not is_useful_origin:
prefix = None
elif self._capabilities.body_markup and self._capabilities.body_hyperlinks:
- href = html.escape(
- origin_url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
- )
+ href = html.escape(origin_url.toString(urlutils.FormatOption.ENCODED))
text = html.escape(urlstr, quote=False)
prefix = f'<a href="{href}">{text}</a>'
elif self._capabilities.body_markup:
diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py
index 38d4eea10..4a09c81fb 100644
--- a/qutebrowser/browser/webengine/webenginequtescheme.py
+++ b/qutebrowser/browser/webengine/webenginequtescheme.py
@@ -17,7 +17,7 @@
"""QtWebEngine specific qute://* handlers and glue code."""
-from qutebrowser.qt.core import QBuffer, QIODevice, QUrl
+from qutebrowser.qt.core import QBuffer, QIODevice, QUrl, QByteArray
from qutebrowser.qt.webenginecore import (QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob,
QWebEngineUrlScheme)
@@ -25,6 +25,9 @@ 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]
+
class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
@@ -33,9 +36,9 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
def install(self, profile):
"""Install the handler for qute:// URLs on the given profile."""
if QWebEngineUrlScheme is not None:
- assert QWebEngineUrlScheme.schemeByName(b'qute') is not None
+ assert QWebEngineUrlScheme.schemeByName(_QUTE) is not None
- profile.installUrlSchemeHandler(b'qute', self)
+ profile.installUrlSchemeHandler(_QUTE, self)
def _check_initiator(self, job):
"""Check whether the initiator of the job should be allowed.
@@ -133,8 +136,8 @@ def init():
classes.
"""
if QWebEngineUrlScheme is not None:
- assert not QWebEngineUrlScheme.schemeByName(b'qute').name()
- scheme = QWebEngineUrlScheme(b'qute')
+ assert not QWebEngineUrlScheme.schemeByName(_QUTE).name()
+ scheme = QWebEngineUrlScheme(_QUTE)
scheme.setFlags(
QWebEngineUrlScheme.Flag.LocalScheme |
QWebEngineUrlScheme.Flag.LocalAccessAllowed)
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index fbffd3091..e55d75ecd 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -37,7 +37,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
webengineinspector)
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
- resources, message, jinja, debug, version)
+ resources, message, jinja, debug, version, urlutils)
from qutebrowser.qt import sip, machinery
from qutebrowser.misc import objects, miscwidgets
@@ -104,6 +104,12 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
self._widget.print(printer)
+if machinery.IS_QT5:
+ _FindFlagType = Union[QWebEnginePage.FindFlag, QWebEnginePage.FindFlags]
+else:
+ _FindFlagType = QWebEnginePage.FindFlag
+
+
@dataclasses.dataclass
class _FindFlags:
@@ -112,13 +118,11 @@ class _FindFlags:
def to_qt(self):
"""Convert flags into Qt flags."""
- # FIXME:mypy Those should be correct, reevaluate with PyQt6-stubs
- flags = QWebEnginePage.FindFlag(0)
+ flags: _FindFlagType = QWebEnginePage.FindFlag(0)
if self.case_sensitive:
- flags |= ( # type: ignore[assignment]
- QWebEnginePage.FindFlag.FindCaseSensitively)
+ flags |= QWebEnginePage.FindFlag.FindCaseSensitively
if self.backward:
- flags |= QWebEnginePage.FindFlag.FindBackward # type: ignore[assignment]
+ flags |= QWebEnginePage.FindFlag.FindBackward
return flags
def __bool__(self):
@@ -1431,8 +1435,7 @@ class WebEngineTab(browsertab.AbstractTab):
title = self.title()
title_url = QUrl(url)
title_url.setScheme('')
- title_url_str = title_url.toDisplayString(
- QUrl.UrlFormattingOption.RemoveScheme) # type: ignore[arg-type]
+ title_url_str = title_url.toDisplayString(urlutils.FormatOption.REMOVE_SCHEME)
if title == title_url_str.strip('/'):
title = ""
diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py
index 09a5a710a..7a08a0736 100644
--- a/qutebrowser/browser/webkit/webview.py
+++ b/qutebrowser/browser/webkit/webview.py
@@ -20,13 +20,13 @@
"""The main browser widgets."""
-from qutebrowser.qt.core import pyqtSignal, Qt, QUrl
+from qutebrowser.qt.core import pyqtSignal, Qt
from qutebrowser.qt.webkit import QWebSettings
from qutebrowser.qt.webkitwidgets import QWebView, QWebPage
from qutebrowser.config import config, stylesheet
from qutebrowser.keyinput import modeman
-from qutebrowser.utils import log, usertypes, utils, objreg, debug
+from qutebrowser.utils import log, usertypes, utils, objreg, debug, urlutils
from qutebrowser.browser.webkit import webpage
@@ -83,8 +83,7 @@ class WebView(QWebView):
stylesheet.set_register(self)
def __repr__(self):
- flags = QUrl.ComponentFormattingOption.EncodeUnicode
- urlstr = self.url().toDisplayString(flags) # type: ignore[arg-type]
+ urlstr = self.url().toDisplayString(urlutils.FormatOption.ENCODE_UNICODE)
url = utils.elide(urlstr, 100)
return utils.get_repr(self, tab_id=self._tab_id, url=url)
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index bedcc2ade..8a255d04c 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -327,6 +327,23 @@ qt.chromium.sandboxing:
- https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox_faq.md[FAQ (Windows-centric)]
# yamllint enable rule:line-length
+qt.chromium.experimental_web_platform_features:
+ type:
+ name: String
+ valid_values:
+ - always: Enable experimental web platform features.
+ - auto: Enable experimental web platform features when using Qt 5.
+ - never: Disable experimental web platform features.
+ default: auto
+ backend: QtWebEngine
+ restart: true
+ desc: >-
+ Enables Web Platform features that are in development.
+
+ This passes the `--enable-experimental-web-platform-features` flag to
+ Chromium. By default, this is enabled with Qt 5 to maximize compatibility
+ despite an aging Chromium base.
+
qt.highdpi:
type: Bool
default: false
@@ -610,15 +627,10 @@ content.site_specific_quirks.skip:
- misc-krunker
- misc-mathml-darkmode
none_ok: true
- default: ["js-string-replaceall"]
+ default: []
desc: >-
Disable a list of named quirks.
- The js-string-replaceall quirk is needed for Nextcloud Calendar < 2.2.0 with
- QtWebEngine < 5.15.3. However, the workaround is not fully compliant to the
- ECMAScript spec and might cause issues on other websites, so it's disabled by
- default.
-
# emacs: '
content.geolocation:
diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py
index 91cb9a17e..5c91b321e 100644
--- a/qutebrowser/config/qtargs.py
+++ b/qutebrowser/config/qtargs.py
@@ -23,6 +23,7 @@ import argparse
import pathlib
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import QLocale
from qutebrowser.config import config
@@ -329,7 +330,13 @@ _WEBENGINE_SETTINGS: Dict[str, Dict[Any, Optional[str]]] = {
'enable-all': None,
'disable-seccomp-bpf': '--disable-seccomp-filter-sandbox',
'disable-all': '--no-sandbox',
- }
+ },
+ 'qt.chromium.experimental_web_platform_features': {
+ 'always': '--enable-experimental-web-platform-features',
+ 'never': None,
+ 'auto':
+ '--enable-experimental-web-platform-features' if machinery.IS_QT5 else None,
+ },
}
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
index 18da02ea4..11bcbffa2 100644
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -259,7 +259,7 @@ def clear_private_data() -> None:
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import cookies
assert cookies.ram_cookie_jar is not None
- cookies.ram_cookie_jar.setAllCookies([])
+ cookies.ram_cookie_jar.setAllCookies([]) # type: ignore[unreachable]
else:
raise utils.Unreachable(objects.backend)
diff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py
index a66797ead..007be6d15 100644
--- a/qutebrowser/keyinput/eventfilter.py
+++ b/qutebrowser/keyinput/eventfilter.py
@@ -19,6 +19,7 @@
from typing import cast
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import pyqtSlot, QObject, QEvent
from qutebrowser.qt.gui import QKeyEvent, QWindow
@@ -85,29 +86,31 @@ class EventFilter(QObject):
Return:
True if the event should be filtered, False if it's passed through.
"""
+ ev_type = event.type()
+ if machinery.IS_QT6:
+ ev_type = cast(QEvent.Type, ev_type)
+
if self._log_qt_events:
try:
source = repr(obj)
except AttributeError: # might not be fully initialized yet
source = type(obj).__name__
- evtype = debug.qenum_key(QEvent.Type, event.type())
- log.misc.debug(f"{source} got event: {evtype}")
+ ev_type_str = debug.qenum_key(QEvent, ev_type)
+ log.misc.debug(f"{source} got event: {ev_type_str}")
if not isinstance(obj, QWindow):
# We already handled this same event at some point earlier, so
# we're not interested in it anymore.
return False
- typ = event.type()
-
- if typ not in self._handlers:
+ if ev_type not in self._handlers:
return False
if not self._activated:
return False
- handler = self._handlers[typ]
+ handler = self._handlers[ev_type]
try:
return handler(cast(QKeyEvent, event))
except:
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
index 3782faa11..e2a15b2c0 100644
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -226,8 +226,8 @@ def _check_valid_utf8(s: str, data: Union[Qt.Key, _ModifierType]) -> None:
try:
s.encode('utf-8')
except UnicodeEncodeError as e: # pragma: no cover
- raise ValueError("Invalid encoding in 0x{:x} -> {}: {}"
- .format(int(data), s, e))
+ i = qtutils.extract_enum_val(data)
+ raise ValueError(f"Invalid encoding in 0x{i:x} -> {s}: {e}")
def _key_to_string(key: Qt.Key) -> str:
@@ -350,8 +350,8 @@ def _unset_modifier_bits(
"""
if machinery.IS_QT5:
return cast(_ModifierType, modifiers & ~mask)
- return Qt.KeyboardModifier( # type: ignore[unreachable]
- modifiers.value & ~mask.value)
+ else:
+ return Qt.KeyboardModifier(modifiers.value & ~mask.value)
@dataclasses.dataclass(frozen=True, order=True)
@@ -397,7 +397,7 @@ class KeyInfo:
except ValueError as ex:
raise InvalidKeyError(str(ex))
key = _remap_unicode(key, e.text())
- modifiers = cast(Qt.KeyboardModifier, e.modifiers())
+ modifiers = e.modifiers()
return cls(key, modifiers)
@classmethod
@@ -673,7 +673,7 @@ class KeySequence:
raise KeyParseError(None, f"Got invalid key: {e}")
_assert_plain_key(key)
- _assert_plain_modifier(cast(Qt.KeyboardModifier, ev.modifiers()))
+ _assert_plain_modifier(ev.modifiers())
key = _remap_unicode(key, ev.text())
modifiers: _ModifierType = ev.modifiers()
@@ -723,7 +723,7 @@ class KeySequence:
modifiers |= Qt.KeyboardModifier.ControlModifier
infos = list(self)
- infos.append(KeyInfo(key, cast(Qt.KeyboardModifier, modifiers)))
+ infos.append(KeyInfo(key, modifiers))
return self.__class__(*infos)
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index 8fdb6bebc..fe3650a2c 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -21,6 +21,7 @@ import functools
import dataclasses
from typing import Mapping, Callable, MutableMapping, Union, Set, cast
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
from qutebrowser.qt.gui import QKeyEvent, QKeySequence
@@ -289,10 +290,18 @@ class ModeManager(QObject):
"{}".format(curmode, utils.qualname(parser)))
match = parser.handle(event, dry_run=dry_run)
- has_modifier = event.modifiers() not in [
- Qt.KeyboardModifier.NoModifier,
- Qt.KeyboardModifier.ShiftModifier,
- ] # type: ignore[comparison-overlap]
+ if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing
+ ignored_modifiers = [
+ cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.NoModifier),
+ cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier),
+ ]
+ else:
+ ignored_modifiers = [
+ Qt.KeyboardModifier.NoModifier,
+ Qt.KeyboardModifier.ShiftModifier,
+ ]
+ has_modifier = event.modifiers() not in ignored_modifiers
+
is_non_alnum = has_modifier or not event.text().strip()
forward_unbound_keys = config.cache['input.forward_unbound_keys']
@@ -465,7 +474,11 @@ class ModeManager(QObject):
QEvent.Type.ShortcutOverride:
functools.partial(self._handle_keypress, dry_run=True),
}
- handler = handlers[event.type()]
+ ev_type = event.type()
+ if machinery.IS_QT6:
+ ev_type = cast(QEvent.Type, ev_type)
+
+ handler = handlers[ev_type]
return handler(cast(QKeyEvent, event))
@cmdutils.register(instance='mode-manager', scope='window')
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index 80875528e..5e34a6649 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -21,8 +21,9 @@ import binascii
import base64
import itertools
import functools
-from typing import List, MutableSequence, Optional, Tuple
+from typing import List, MutableSequence, Optional, Tuple, cast
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import (pyqtBoundSignal, pyqtSlot, QRect, QPoint, QTimer, Qt,
QCoreApplication, QEventLoop, QByteArray)
from qutebrowser.qt.widgets import QWidget, QVBoxLayout, QSizePolicy
@@ -574,11 +575,15 @@ class MainWindow(QWidget):
def _set_decoration(self, hidden):
"""Set the visibility of the window decoration via Qt."""
- window_flags = Qt.WindowType.Window
+ if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing
+ window_flags = cast(Qt.WindowFlags, Qt.WindowType.Window)
+ else:
+ window_flags = Qt.WindowType.Window
+
refresh_window = self.isVisible()
if hidden:
modifiers = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.NoDropShadowWindowHint
- window_flags |= modifiers # type: ignore[assignment]
+ window_flags |= modifiers
self.setWindowFlags(window_flags)
if utils.is_mac and hidden and not qtutils.version_check('6.3', compiled=False):
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index 53eda2e27..26a2ae886 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -187,7 +187,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) # type: ignore[arg-type]
+ loop.exec(flags)
log.prompt.debug("Ending loop.exec() for {}".format(question))
log.prompt.debug("Restoring old question {}".format(old_question))
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index 76e5ebaeb..9abfe7152 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -20,7 +20,7 @@
import enum
import dataclasses
-from qutebrowser.qt.core import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
+from qutebrowser.qt.core import pyqtSignal, pyqtProperty, pyqtSlot, Qt, QSize, QTimer
from qutebrowser.qt.widgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.browser import browsertab
diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py
index c546e29aa..4332316a3 100644
--- a/qutebrowser/mainwindow/statusbar/command.py
+++ b/qutebrowser/mainwindow/statusbar/command.py
@@ -17,8 +17,9 @@
"""The commandline in the statusbar."""
-from typing import Optional
+from typing import Optional, cast
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, Qt, QSize
from qutebrowser.qt.gui import QKeyEvent
from qutebrowser.qt.widgets import QSizePolicy, QWidget
@@ -267,6 +268,11 @@ class Command(misc.CommandLineEdit):
Enter/Shift+Enter/etc. will cause QLineEdit to think it's finished
without command_accept to be called.
"""
+ if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing
+ shift = cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier)
+ else:
+ shift = Qt.KeyboardModifier.ShiftModifier
+
text = self.text()
if text in modeparsers.STARTCHARS and e.key() == Qt.Key.Key_Backspace:
e.accept()
@@ -274,7 +280,7 @@ class Command(misc.CommandLineEdit):
'prefix deleted')
elif e.key() == Qt.Key.Key_Return:
e.ignore()
- elif e.key() == Qt.Key.Key_Insert and e.modifiers() == Qt.KeyboardModifier.ShiftModifier: # type: ignore[comparison-overlap]
+ elif e.key() == Qt.Key.Key_Insert and e.modifiers() == shift:
try:
text = utils.get_clipboard(selection=True, fallback=True)
except utils.ClipboardError:
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 566bd8afa..da3392a7e 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -219,7 +219,8 @@ class TabbedBrowser(QWidget):
self._tab_insert_idx_right = -1
self.is_shutting_down = False
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
- self.widget.new_tab_requested.connect(self.tabopen)
+ self.widget.new_tab_requested.connect(
+ self.tabopen) # type: ignore[arg-type,unused-ignore]
self.widget.currentChanged.connect(self._on_current_changed)
self.cur_fullscreen_requested.connect(self.widget.tab_bar().maybe_hide)
diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py
index 04c92a529..416072ccb 100644
--- a/qutebrowser/misc/crashdialog.py
+++ b/qutebrowser/misc/crashdialog.py
@@ -631,7 +631,8 @@ class ReportErrorDialog(QDialog):
hbox = QHBoxLayout()
hbox.addStretch()
btn = QPushButton("Close")
- btn.clicked.connect(self.close)
+ # FIXME:mypy PyQt6-stubs issue
+ btn.clicked.connect(self.close) # type: ignore[arg-type,unused-ignore]
hbox.addWidget(btn)
vbox.addLayout(hbox)
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index 04e5c44a5..a0265d653 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -284,12 +284,9 @@ def configure_pyqt():
pyqtRemoveInputHook()
from qutebrowser.qt import sip
- try:
- sip.enableoverflowchecking(True)
- except AttributeError:
+ if machinery.IS_QT5:
# default in PyQt6
- # FIXME:qt6 solve this in qutebrowser/qt/sip.py equivalent?
- pass
+ sip.enableoverflowchecking(True)
def init_log(args):
diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py
index 2bb152b03..1dddddba7 100644
--- a/qutebrowser/misc/httpclient.py
+++ b/qutebrowser/misc/httpclient.py
@@ -78,7 +78,9 @@ class HTTPClient(QObject):
request = HTTPRequest(url)
request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader,
'application/x-www-form-urlencoded;charset=utf-8')
- reply = self._nam.post(request, encoded_data)
+ # FIXME:mypy PyQt6-stubs issue
+ reply = self._nam.post( # type: ignore[call-overload,unused-ignore]
+ request, encoded_data)
self._handle_reply(reply)
def get(self, url):
diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py
index e4a8e5d70..fb1b1ac22 100644
--- a/qutebrowser/misc/ipc.py
+++ b/qutebrowser/misc/ipc.py
@@ -274,12 +274,15 @@ class IPCServer(QObject):
socket.errorOccurred.connect(self.on_error)
- if socket.error() not in [ # type: ignore[operator]
+ # FIXME:v4 Ignore needed due to overloaded signal/method in Qt 5
+ socket_error = socket.error() # type: ignore[operator,unused-ignore]
+ if socket_error not in [
QLocalSocket.LocalSocketError.UnknownSocketError,
QLocalSocket.LocalSocketError.PeerClosedError
]:
log.ipc.debug("We got an error immediately.")
- self.on_error(socket.error()) # type: ignore[operator]
+ self.on_error(socket_error)
+
socket.disconnected.connect(self.on_disconnected)
if socket.state() == QLocalSocket.LocalSocketState.UnconnectedState:
log.ipc.debug("Socket was disconnected immediately.")
diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py
index 652735f2f..ba33da775 100644
--- a/qutebrowser/misc/miscwidgets.py
+++ b/qutebrowser/misc/miscwidgets.py
@@ -462,8 +462,8 @@ class KeyTesterWidget(QWidget):
lines = [
str(keyutils.KeyInfo.from_event(e)),
'',
- f"key: {debug.qenum_key(Qt.Key, e.key(), klass=Qt.Key)}",
- f"modifiers: {debug.qflags_key(Qt.KeyboardModifier, e.modifiers())}",
+ f"key: {debug.qenum_key(Qt, e.key(), klass=Qt.Key)}",
+ f"modifiers: {debug.qflags_key(Qt, e.modifiers())}",
'text: {!r}'.format(e.text()),
]
self._label.setText('\n'.join(lines))
diff --git a/qutebrowser/misc/nativeeventfilter.py b/qutebrowser/misc/nativeeventfilter.py
index e96c61682..4562ea82d 100644
--- a/qutebrowser/misc/nativeeventfilter.py
+++ b/qutebrowser/misc/nativeeventfilter.py
@@ -20,12 +20,12 @@
This entire file is a giant WORKAROUND for https://bugreports.qt.io/browse/QTBUG-114334.
"""
-from typing import Tuple, Union
+from typing import Tuple, Union, cast
import enum
import ctypes
import ctypes.util
-from qutebrowser.qt import sip
+from qutebrowser.qt import sip, machinery
from qutebrowser.qt.core import QAbstractNativeEventFilter, QByteArray, qVersion
from qutebrowser.misc import objects
@@ -103,6 +103,12 @@ class xcb_query_extension_reply_t(ctypes.Structure): # noqa: N801
# pylint: enable=invalid-name
+if machinery.IS_QT6:
+ _PointerRetType = sip.voidptr
+else:
+ _PointerRetType = int
+
+
class NativeEventFilter(QAbstractNativeEventFilter):
"""Event filter for XCB messages to work around Qt 6.5.1 crash."""
@@ -111,8 +117,8 @@ class NativeEventFilter(QAbstractNativeEventFilter):
#
# Tuple because PyQt uses the second value as the *result out-pointer, which
# according to the Qt documentation is only used on Windows.
- _PASS_EVENT_RET = (False, 0)
- _FILTER_EVENT_RET = (True, 0)
+ _PASS_EVENT_RET = (False, cast(_PointerRetType, 0))
+ _FILTER_EVENT_RET = (True, cast(_PointerRetType, 0))
def __init__(self) -> None:
super().__init__()
@@ -145,7 +151,7 @@ class NativeEventFilter(QAbstractNativeEventFilter):
def nativeEventFilter(
self, evtype: Union[bytes, QByteArray], message: sip.voidptr
- ) -> Tuple[bool, int]:
+ ) -> Tuple[bool, _PointerRetType]:
"""Handle XCB events."""
# We're only installed when the platform plugin is xcb
assert evtype == b"xcb_generic_event_t", evtype
diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py
index 4892b342b..194b20da5 100644
--- a/qutebrowser/misc/sql.py
+++ b/qutebrowser/misc/sql.py
@@ -347,7 +347,7 @@ class Query:
msg = f'Failed to {step} query "{query}": "{error.text()}"'
raise_sqlite_error(msg, error)
- def _validate_bound_values(self):
+ def _validate_bound_values(self) -> None:
"""Make sure all placeholders are bound."""
qt_bound_values = self.query.boundValues()
if machinery.IS_QT5:
diff --git a/qutebrowser/qt/_core_pyqtproperty.py b/qutebrowser/qt/_core_pyqtproperty.py
new file mode 100644
index 000000000..c6943cc78
--- /dev/null
+++ b/qutebrowser/qt/_core_pyqtproperty.py
@@ -0,0 +1,78 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+"""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
+"""
+
+# flake8: noqa
+# pylint: disable=invalid-name,missing-class-docstring,too-many-arguments,redefined-builtin,unused-argument,import-error
+
+import typing
+from PyQt6.QtCore import QObjectT, pyqtSignal
+
+if typing.TYPE_CHECKING:
+ TPropertyTypeVal = typing.TypeVar("TPropertyTypeVal")
+
+ TPropGetter = typing.TypeVar(
+ "TPropGetter", bound=typing.Callable[[QObjectT], TPropertyTypeVal]
+ )
+ TPropSetter = typing.TypeVar(
+ "TPropSetter", bound=typing.Callable[[QObjectT, TPropertyTypeVal], None]
+ )
+ TPropDeleter = typing.TypeVar(
+ "TPropDeleter", bound=typing.Callable[[QObjectT], None]
+ )
+ TPropResetter = typing.TypeVar(
+ "TPropResetter", bound=typing.Callable[[QObjectT], None]
+ )
+
+ class pyqtProperty:
+ def __init__(
+ self,
+ type: typing.Union[type, str],
+ fget: typing.Optional[typing.Callable[[QObjectT], TPropertyTypeVal]] = None,
+ fset: typing.Optional[
+ typing.Callable[[QObjectT, TPropertyTypeVal], None]
+ ] = None,
+ freset: typing.Optional[typing.Callable[[QObjectT], None]] = None,
+ fdel: typing.Optional[typing.Callable[[QObjectT], None]] = None,
+ doc: typing.Optional[str] = "",
+ designable: bool = True,
+ scriptable: bool = True,
+ stored: bool = True,
+ user: bool = True,
+ constant: bool = True,
+ final: bool = True,
+ notify: typing.Optional[pyqtSignal] = None,
+ revision: int = 0,
+ ) -> None:
+ ...
+
+ type: typing.Union[type, str]
+ fget: typing.Optional[typing.Callable[[], TPropertyTypeVal]]
+ fset: typing.Optional[typing.Callable[[TPropertyTypeVal], None]]
+ freset: typing.Optional[typing.Callable[[], None]]
+ fdel: typing.Optional[typing.Callable[[], None]]
+
+ def read(self, func: TPropGetter) -> "pyqtProperty":
+ ...
+
+ def write(self, func: TPropSetter) -> "pyqtProperty":
+ ...
+
+ def reset(self, func: TPropResetter) -> "pyqtProperty":
+ ...
+
+ def getter(self, func: TPropGetter) -> "pyqtProperty":
+ ...
+
+ def setter(self, func: TPropSetter) -> "pyqtProperty":
+ ...
+
+ def deleter(self, func: TPropDeleter) -> "pyqtProperty":
+ ...
+
+ def __call__(self, func: TPropGetter) -> "pyqtProperty":
+ ...
diff --git a/qutebrowser/qt/core.py b/qutebrowser/qt/core.py
index 6ae147ce7..87a253218 100644
--- a/qutebrowser/qt/core.py
+++ b/qutebrowser/qt/core.py
@@ -11,6 +11,7 @@ Any API exported from this module is based on the Qt 6 API:
https://doc.qt.io/qt-6/qtcore-index.html
"""
+from typing import TYPE_CHECKING
from qutebrowser.qt import machinery
machinery.init_implicit()
@@ -22,5 +23,8 @@ elif machinery.USE_PYQT5:
from PyQt5.QtCore import *
elif machinery.USE_PYQT6:
from PyQt6.QtCore import *
+
+ if TYPE_CHECKING:
+ from qutebrowser.qt._core_pyqtproperty import pyqtProperty
else:
raise machinery.UnknownWrapper()
diff --git a/qutebrowser/qt/sip.py b/qutebrowser/qt/sip.py
index 113fde629..ab5d9b907 100644
--- a/qutebrowser/qt/sip.py
+++ b/qutebrowser/qt/sip.py
@@ -31,6 +31,6 @@ elif machinery.USE_PYQT6:
# While upstream recommends using PyQt5.sip ever since PyQt5 5.11, some
# distributions still package later versions of PyQt5 with a top-level
# "sip" rather than "PyQt5.sip".
- from sip import *
+ from sip import * # type: ignore[import]
else:
raise machinery.UnknownWrapper()
diff --git a/qutebrowser/qt/webkit.py b/qutebrowser/qt/webkit.py
index 58f706d5d..c4b0bb7ae 100644
--- a/qutebrowser/qt/webkit.py
+++ b/qutebrowser/qt/webkit.py
@@ -12,6 +12,7 @@ Any API exported from this module is based on the QtWebKit 5.212 API:
https://qtwebkit.github.io/doc/qtwebkit/qtwebkit-index.html
"""
+import typing
from qutebrowser.qt import machinery
machinery.init_implicit()
@@ -19,7 +20,12 @@ machinery.init_implicit()
if machinery.USE_PYSIDE6: # pylint: disable=no-else-raise
raise machinery.Unavailable()
-elif machinery.USE_PYQT5:
+elif machinery.USE_PYQT5 or typing.TYPE_CHECKING:
+ # If we use mypy (even on Qt 6), we pretend to have WebKit available.
+ # This avoids central API (like BrowserTab) being Any because the webkit part of
+ # the unions there is missing.
+ # This causes various issues inside browser/webkit/, but we ignore those in
+ # .mypy.ini because we don't really care much about QtWebKit anymore.
from PyQt5.QtWebKit import *
elif machinery.USE_PYQT6:
raise machinery.Unavailable()
diff --git a/qutebrowser/qt/webkitwidgets.py b/qutebrowser/qt/webkitwidgets.py
index 4a3c61b4f..5b790dcc7 100644
--- a/qutebrowser/qt/webkitwidgets.py
+++ b/qutebrowser/qt/webkitwidgets.py
@@ -12,6 +12,8 @@ Any API exported from this module is based on the QtWebKit 5.212 API:
https://qtwebkit.github.io/doc/qtwebkit/qtwebkitwidgets-index.html
"""
+import typing
+
from qutebrowser.qt import machinery
machinery.init_implicit()
@@ -19,7 +21,12 @@ machinery.init_implicit()
if machinery.USE_PYSIDE6:
raise machinery.Unavailable()
-elif machinery.USE_PYQT5:
+elif machinery.USE_PYQT5 or typing.TYPE_CHECKING:
+ # If we use mypy (even on Qt 6), we pretend to have WebKit available.
+ # This avoids central API (like BrowserTab) being Any because the webkit part of
+ # the unions there is missing.
+ # This causes various issues inside browser/webkit/, but we ignore those in
+ # .mypy.ini because we don't really care much about QtWebKit anymore.
from PyQt5.QtWebKitWidgets import *
elif machinery.USE_PYQT6:
raise machinery.Unavailable()
diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py
index 74e3e8ba7..e778cc23a 100644
--- a/qutebrowser/qutebrowser.py
+++ b/qutebrowser/qutebrowser.py
@@ -102,12 +102,6 @@ def get_argparser():
help=argparse.SUPPRESS,
action='store_true')
- # WORKAROUND to be able to restart from older qutebrowser versions into this one.
- # Should be removed at some point.
- parser.add_argument('--enable-webengine-inspector',
- help=argparse.SUPPRESS,
- action='store_true')
-
debug = parser.add_argument_group('debug arguments')
debug.add_argument('-l', '--loglevel', dest='loglevel',
help="Override the configured console loglevel",
diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py
index ec1350ff4..a8b436d79 100644
--- a/qutebrowser/utils/debug.py
+++ b/qutebrowser/utils/debug.py
@@ -31,7 +31,7 @@ from qutebrowser.qt.core import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSigna
from qutebrowser.utils import log, utils, qtutils, objreg
from qutebrowser.misc import objects
-from qutebrowser.qt import sip
+from qutebrowser.qt import sip, machinery
def log_events(klass: Type[QObject]) -> Type[QObject]:
@@ -97,7 +97,10 @@ def log_signals(obj: QObject) -> QObject:
return obj
-_EnumValueType = Union[sip.simplewrapper, int]
+if machinery.IS_QT6:
+ _EnumValueType = Union[enum.Enum, int]
+else:
+ _EnumValueType = Union[sip.simplewrapper, int]
def _qenum_key_python(
@@ -122,14 +125,14 @@ def _qenum_key_python(
def _qenum_key_qt(
- base: Type[_EnumValueType],
+ base: Type[sip.simplewrapper],
value: _EnumValueType,
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
try:
- meta_obj = base.staticMetaObject # type: ignore[union-attr]
+ meta_obj = base.staticMetaObject # type: ignore[attr-defined]
idx = meta_obj.indexOfEnumerator(klass.__name__)
meta_enum = meta_obj.enumerator(idx)
key = meta_enum.valueToKey(int(value)) # type: ignore[arg-type]
@@ -147,7 +150,7 @@ def _qenum_key_qt(
def qenum_key(
- base: Type[_EnumValueType],
+ base: Type[sip.simplewrapper],
value: _EnumValueType,
klass: Type[_EnumValueType] = None,
) -> str:
@@ -181,7 +184,7 @@ def qenum_key(
return '0x{:04x}'.format(int(value)) # type: ignore[arg-type]
-def qflags_key(base: Type[_EnumValueType],
+def qflags_key(base: Type[sip.simplewrapper],
value: _EnumValueType,
klass: Type[_EnumValueType] = None) -> str:
"""Convert a Qt QFlags value to its keys as string.
@@ -214,15 +217,15 @@ def qflags_key(base: Type[_EnumValueType],
bits = []
names = []
mask = 0x01
- value = qtutils.extract_enum_val(value)
- while mask <= value:
- if value & mask:
+ intval = qtutils.extract_enum_val(value)
+ while mask <= intval:
+ if intval & mask:
bits.append(mask)
mask <<= 1
for bit in bits:
# We have to re-convert to an enum type here or we'll sometimes get an
# empty string back.
- enum_value = klass(bit) # type: ignore[call-arg]
+ enum_value = klass(bit) # type: ignore[call-arg,unused-ignore]
names.append(qenum_key(base, enum_value, klass))
return '|'.join(names)
diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py
index cde8b3442..f8d5b0d1a 100644
--- a/qutebrowser/utils/jinja.py
+++ b/qutebrowser/utils/jinja.py
@@ -112,7 +112,7 @@ class Environment(jinja2.Environment):
url = QUrl('qute://resource')
url.setPath('/' + path)
urlutils.ensure_valid(url)
- urlstr = url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
+ urlstr = url.toString(urlutils.FormatOption.ENCODED)
return urlstr
def _data_url(self, path: str) -> str:
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 6bf7f143b..f9d880612 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -31,18 +31,17 @@ import json
import inspect
import argparse
from typing import (TYPE_CHECKING, Any, Iterator, Mapping, MutableSequence,
- Optional, Set, Tuple, Union)
+ Optional, Set, Tuple, Union, TextIO, cast)
from qutebrowser.qt import core as qtcore
# Optional imports
try:
import colorama
except ImportError:
- colorama = None
+ colorama = None # type: ignore[assignment]
if TYPE_CHECKING:
from qutebrowser.config import config as configmodule
- from typing import TextIO
_log_inited = False
_args = None
@@ -277,7 +276,7 @@ def _init_handlers(
else:
strip = False if force_color else None
if use_colorama:
- stream = colorama.AnsiToWin32(sys.stderr, strip=strip)
+ stream = cast(TextIO, colorama.AnsiToWin32(sys.stderr, strip=strip))
else:
stream = sys.stderr
console_handler = logging.StreamHandler(stream)
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index d8abfad93..340ae520b 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -45,7 +45,7 @@ except ImportError: # pragma: no cover
qWebKitVersion = None # type: ignore[assignment] # noqa: N816
if TYPE_CHECKING:
from qutebrowser.qt.webkit import QWebHistory
- from qutebrowser.qt.webenginewidgets import QWebEngineHistory
+ from qutebrowser.qt.webenginecore import QWebEngineHistory
from qutebrowser.misc import objects
from qutebrowser.utils import usertypes, utils
@@ -457,7 +457,8 @@ class QtValueError(ValueError):
if machinery.IS_QT6:
_ProcessEventFlagType = QEventLoop.ProcessEventsFlag
else:
- _ProcessEventFlagType = QEventLoop.ProcessEventsFlags
+ _ProcessEventFlagType = Union[
+ QEventLoop.ProcessEventsFlag, QEventLoop.ProcessEventsFlags]
class EventLoop(QEventLoop):
@@ -472,15 +473,15 @@ class EventLoop(QEventLoop):
self._executing = False
def exec(
- self,
- flags: _ProcessEventFlagType = (
- QEventLoop.ProcessEventsFlag.AllEvents # type: ignore[assignment]
- ),
+ self,
+ flags: _ProcessEventFlagType = QEventLoop.ProcessEventsFlag.AllEvents,
) -> int:
"""Override exec_ to raise an exception when re-running."""
if self._executing:
raise AssertionError("Eventloop is already running!")
self._executing = True
+ if machinery.IS_QT5:
+ flags = cast(QEventLoop.ProcessEventsFlags, flags)
status = super().exec(flags)
self._executing = False
return status
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index a67f700d7..e00c9dab2 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -24,8 +24,9 @@ import ipaddress
import posixpath
import urllib.parse
import mimetypes
-from typing import Optional, Tuple, Union, Iterable
+from typing import Optional, Tuple, Union, Iterable, cast
+from qutebrowser.qt import machinery
from qutebrowser.qt.core import QUrl
from qutebrowser.qt.network import QHostInfo, QHostAddress, QNetworkProxy
@@ -39,6 +40,55 @@ from qutebrowser.browser.network import pac
# https://github.com/qutebrowser/qutebrowser/issues/108
+if machinery.IS_QT6:
+ UrlFlagsType = Union[QUrl.UrlFormattingOption, QUrl.ComponentFormattingOption]
+
+ class FormatOption:
+ """Simple wrapper around Qt enums to fix typing problems on Qt 5."""
+
+ ENCODED = QUrl.ComponentFormattingOption.FullyEncoded
+ ENCODE_UNICODE = QUrl.ComponentFormattingOption.EncodeUnicode
+ DECODE_RESERVED = QUrl.ComponentFormattingOption.DecodeReserved
+
+ REMOVE_SCHEME = QUrl.UrlFormattingOption.RemoveScheme
+ REMOVE_PASSWORD = QUrl.UrlFormattingOption.RemovePassword
+else:
+ UrlFlagsType = Union[
+ QUrl.FormattingOptions,
+ QUrl.UrlFormattingOption,
+ QUrl.ComponentFormattingOption,
+ QUrl.ComponentFormattingOptions,
+ ]
+
+ class _QtFormattingOptions(QUrl.FormattingOptions):
+ """WORKAROUND for invalid stubs.
+
+ Teach mypy that | works for QUrl.FormattingOptions.
+ """
+
+ def __or__(self, f: UrlFlagsType) -> '_QtFormattingOptions':
+ return super() | f # type: ignore[operator,return-value]
+
+ class FormatOption:
+ """WORKAROUND for invalid stubs.
+
+ Pretend that all ComponentFormattingOption values are also valid
+ QUrl.FormattingOptions values, i.e. can be passed to QUrl.toString().
+ """
+
+ ENCODED = cast(
+ _QtFormattingOptions, QUrl.ComponentFormattingOption.FullyEncoded)
+ ENCODE_UNICODE = cast(
+ _QtFormattingOptions, QUrl.ComponentFormattingOption.EncodeUnicode)
+ DECODE_RESERVED = cast(
+ _QtFormattingOptions, QUrl.ComponentFormattingOption.DecodeReserved)
+
+ REMOVE_SCHEME = cast(
+ _QtFormattingOptions, QUrl.UrlFormattingOption.RemoveScheme)
+ REMOVE_PASSWORD = cast(
+ _QtFormattingOptions, QUrl.UrlFormattingOption.RemovePassword)
+
+
# URL schemes supported by QtWebEngine
WEBENGINE_SCHEMES = [
'about',
@@ -503,9 +553,10 @@ def same_domain(url1: QUrl, url2: QUrl) -> bool:
#
# There are no other callers of same_domain, and url2 will only be ever valid when
# we use a NetworkManager from QtWebKit. However, QtWebKit is Qt 5 only.
+ assert machinery.IS_QT5, machinery.INFO
- suffix1 = url1.topLevelDomain()
- suffix2 = url2.topLevelDomain()
+ suffix1 = url1.topLevelDomain() # type: ignore[attr-defined,unused-ignore]
+ suffix2 = url2.topLevelDomain() # type: ignore[attr-defined,unused-ignore]
if not suffix1:
return url1.host() == url2.host()
@@ -533,7 +584,7 @@ def file_url(path: str) -> str:
path: The absolute path to the local file
"""
url = QUrl.fromLocalFile(path)
- return url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
+ return url.toString(FormatOption.ENCODED)
def data_url(mimetype: str, data: bytes) -> QUrl:
@@ -624,7 +675,7 @@ def parse_javascript_url(url: QUrl) -> str:
raise Error("URL contains unexpected components: {}"
.format(url.authority()))
- urlstr = url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
+ urlstr = url.toString(FormatOption.ENCODED)
urlstr = urllib.parse.unquote(urlstr)
code = urlstr[len('javascript:'):]
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 857bf4d4a..782261745 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,
+from typing import (Mapping, Optional, Sequence, Tuple, ClassVar, Dict, cast, Any,
TYPE_CHECKING)
from qutebrowser.qt import machinery
@@ -1035,9 +1035,8 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
vf = ctx.versionFunctions(vp)
else:
# Qt 6
- # FIXME:qt6 (lint)
from qutebrowser.qt.opengl import QOpenGLVersionFunctionsFactory
- vf = QOpenGLVersionFunctionsFactory.get(vp, ctx)
+ vf: Any = QOpenGLVersionFunctionsFactory.get(vp, ctx)
except ImportError as e:
log.init.debug("Importing version functions failed: {}".format(e))
return None
@@ -1046,6 +1045,7 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
log.init.debug("Getting version functions failed!")
return None
+ # FIXME:mypy PyQt6-stubs issue?
vendor = vf.glGetString(vf.GL_VENDOR)
version = vf.glGetString(vf.GL_VERSION)
diff --git a/scripts/dev/changelog_urls.json b/scripts/dev/changelog_urls.json
index 1c9528f22..baf9e2583 100644
--- a/scripts/dev/changelog_urls.json
+++ b/scripts/dev/changelog_urls.json
@@ -25,6 +25,10 @@
"hypothesis": "https://hypothesis.readthedocs.io/en/latest/changes.html",
"mypy": "https://mypy-lang.blogspot.com/",
"types-PyYAML": "https://github.com/python/typeshed/commits/main/stubs/PyYAML",
+ "types-colorama": "https://github.com/python/typeshed/commits/main/stubs/colorama",
+ "types-docutils": "https://github.com/python/typeshed/commits/main/stubs/docutils",
+ "types-Pygments": "https://github.com/python/typeshed/commits/main/stubs/Pygments",
+ "types-setuptools": "https://github.com/python/typeshed/commits/main/stubs/setuptools",
"pytest": "https://docs.pytest.org/en/latest/changelog.html",
"iniconfig": "https://github.com/pytest-dev/iniconfig/blob/master/CHANGELOG",
"tox": "https://tox.readthedocs.io/en/latest/changelog.html",
@@ -100,6 +104,7 @@
"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",
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index a1df9e51d..240b5e6f1 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -92,6 +92,9 @@ def check_changelog_urls(_args: argparse.Namespace = None) -> bool:
req, _version = recompile_requirements.parse_versioned_line(line)
if req.startswith('./'):
continue
+ if " @ " in req: # vcs URL
+ req = req.split(" @ ")[0]
+
all_requirements.add(req)
if req not in recompile_requirements.CHANGELOG_URLS:
missing.add(req)
@@ -266,6 +269,7 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]:
pathlib.Path('scripts', 'dev', 'misc_checks.py'),
pathlib.Path('scripts', 'dev', 'enums.txt'),
pathlib.Path('qutebrowser', '3rdparty', 'pdfjs'),
+ pathlib.Path('qutebrowser', 'qt', '_core_pyqtproperty.py'),
hint_data / 'ace' / 'ace.js',
hint_data / 'bootstrap' / 'bootstrap.css',
]
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index a92a0f27e..960b5a514 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -188,7 +188,10 @@ def run(files):
whitelist_file.close()
vult = vulture.Vulture(verbose=False)
- vult.scavenge(files + [whitelist_file.name])
+ vult.scavenge(
+ files + [whitelist_file.name],
+ exclude=["qutebrowser/qt/_core_pyqtproperty.py"],
+ )
os.remove(whitelist_file.name)
diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py
index fc16b59d9..c4154947a 100644
--- a/tests/unit/config/test_qtargs.py
+++ b/tests/unit/config/test_qtargs.py
@@ -21,6 +21,7 @@ import logging
import pytest
+from qutebrowser.qt import machinery
from qutebrowser import qutebrowser
from qutebrowser.config import qtargs, configdata
from qutebrowser.utils import usertypes, version
@@ -487,6 +488,20 @@ class TestWebEngineArgs:
args = qtargs.qt_args(parsed)
assert '--lang=de' in args
+ @pytest.mark.parametrize('value, has_arg', [
+ ('always', True),
+ ('auto', machinery.IS_QT5),
+ ('never', False),
+ ])
+ def test_experimental_web_platform_features(
+ self, value, has_arg, parser, config_stub,
+ ):
+ config_stub.val.qt.chromium.experimental_web_platform_features = value
+
+ parsed = parser.parse_args([])
+ args = qtargs.qt_args(parsed)
+ assert ('--enable-experimental-web-platform-features' in args) == has_arg
+
@pytest.mark.parametrize("version, expected", [
('5.15.2', False),
('5.15.9', False),