diff options
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 @@ -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), |