diff options
101 files changed, 1161 insertions, 751 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64dddd2f8..580e532f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,8 @@ jobs: - name: Install dependencies run: | [[ ${{ matrix.testenv }} == eslint ]] && npm install -g eslint - [[ ${{ matrix.testenv }} == docs ]] && sudo apt-get update && sudo apt-get install --no-install-recommends asciidoc + [[ ${{ matrix.testenv }} == docs ]] && sudo apt-get update && sudo apt-get install --no-install-recommends asciidoc libegl1-mesa + [[ ${{ matrix.testenv }} == vulture || ${{ matrix.testenv }} == pylint ]] && sudo apt-get update && sudo apt-get install --no-install-recommends libegl1-mesa if [[ ${{ matrix.testenv }} == shellcheck ]]; then scversion="stable" bindir="$HOME/.local/bin" @@ -89,17 +90,16 @@ jobs: fail-fast: false matrix: include: - - testenv: py + - testenv: py-qt5 image: archlinux-webkit - - testenv: py + - testenv: py-qt5 image: archlinux-webengine - - testenv: py-qt6 + - testenv: py-qt5 + image: archlinux-webengine-unstable + - testenv: py image: archlinux-webengine-qt6 - testenv: py - image: archlinux-webengine-unstable - args: "" - # - testenv: py - # image: archlinux-webengine-unstable-qt6 # FIXME:qt6.5 activate + image: archlinux-webengine-unstable-qt6 container: image: "qutebrowser/ci:${{ matrix.image }}" env: @@ -115,9 +115,9 @@ jobs: with: persist-credentials: false - name: Set up problem matchers - run: "python scripts/dev/ci/problemmatchers.py py38 ${{ runner.temp }}" + run: "python scripts/dev/ci/problemmatchers.py tests ${{ runner.temp }}" - name: Run tox - run: "dbus-run-session -- tox -e ${{ matrix.testenv }} -- ${{ matrix.args }}" + run: "dbus-run-session -- tox -e ${{ matrix.testenv }}" tests: if: "!contains(github.event.head_commit.message, '[ci skip]')" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cabf2d8c4..68d2243a4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,6 +15,7 @@ jobs: - archlinux-webkit - archlinux-webengine - archlinux-webengine-unstable + - archlinux-webengine-unstable-qt6 - archlinux-webengine-qt6 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2254abb4a..c1a8dda8a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,50 +16,50 @@ jobs: include: - os: macos-11 branch: master - toxenv: build-release - name: macos + toxenv: build-release-qt5 + name: qt5-macos - os: windows-2019 args: --64bit branch: master - toxenv: build-release - name: windows-64bit + toxenv: build-release-qt5 + name: qt5-windows-64bit - os: windows-2019 args: --32bit branch: master - toxenv: build-release - name: windows-32bit + toxenv: build-release-qt5 + name: qt5-windows-32bit - os: macos-11 args: --debug branch: master - toxenv: build-release - name: macos-debug + toxenv: build-release-qt5 + name: qt5-macos-debug - os: windows-2019 args: --64bit --debug branch: master - toxenv: build-release - name: windows-64bit-debug + toxenv: build-release-qt5 + name: qt5-windows-64bit-debug - os: windows-2019 args: --32bit --debug branch: master - toxenv: build-release - name: windows-32bit-debug + toxenv: build-release-qt5 + name: qt5-windows-32bit-debug - os: macos-11 - toxenv: build-release-qt6 - name: qt6-macos + toxenv: build-release + name: macos - os: windows-2019 args: --64bit - toxenv: build-release-qt6 - name: qt6-windows-64bit + toxenv: build-release + name: windows-64bit - os: macos-11 args: --debug - toxenv: build-release-qt6 - name: qt6-macos-debug + toxenv: build-release + name: macos-debug - os: windows-2019 args: --64bit --debug - toxenv: build-release-qt6 - name: qt6-windows-64bit-debug + toxenv: build-release + name: windows-64bit-debug runs-on: "${{ matrix.os }}" timeout-minutes: 45 steps: @@ -1,6 +1,6 @@ [MASTER] ignore=resources.py -extension-pkg-whitelist=PyQt5,sip +extension-pkg-whitelist=PyQt5,PyQt6,sip load-plugins=qute_pylint.config, pylint.extensions.docstyle, pylint.extensions.emptystring, @@ -58,8 +58,8 @@ disable=locally-disabled, missing-type-doc, missing-param-doc, useless-param-doc, - wrong-import-order, # FIXME:qt6 (lint) - ungrouped-imports, # FIXME:qt6 (lint) + wrong-import-order, # doesn't work with qutebrowser.qt, even with known-third-party set + ungrouped-imports, # ditto [BASIC] function-rgx=[a-z_][a-z0-9_]{2,50}$ diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 666e24177..7eece0317 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -19,6 +19,17 @@ breaking changes (such as renamed commands) can happen in minor releases. v3.0.0 (unreleased) ------------------- +Major changes +~~~~~~~~~~~~~ + +- qutebrowser now supports Qt 6 and uses it by default. Qt 5.15 is used as a + fallback if Qt 6 is unavailable. This behavior can be customized in three ways + (in order of precedence): + * Via `--qt-wrapper PyQt5` or `--qt-wrapper PyQt6` command-line arguments. + * Via the `QUTE_QT_WRAPPER` environment variable, set to `PyQt6` or `PyQt5`. + * For packagers wanting to provide packages specific to a Qt version, + patch `qutebrowser/qt/machinery.py` and set `_WRAPPER_OVERRIDE`. + Added ~~~~~ @@ -148,7 +159,7 @@ Changed the correct PID (rather than always showing the latest process, which might not be the failing one) - When a process got killed with `SIGTERM`, no error message is now displayed - anymore (unless started with `:spawn --verbose`). + anymore (unless started with `:spawn --verbose`). - When a process got killed by a signal, the signal name is now displayed in the message. - The qute-pass will now try looking up candidate pass entries based on the @@ -177,6 +188,7 @@ Fixed - Multiple rare crashes when quitting qutebrowser. - The `asciidoc2html.py` script now correctly uses the virtualenv-installed asciidoc rather than requiring a system-wide installation. +- Crash when shutting down before fully initialized - Crash with some notification servers when the server is quitting. - Crash when using QtWebKit with PAC and the file has an invalid encoding. - Crash with the "tiramisu" notification server. diff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec index 467994bab..1eee9161d 100644 --- a/misc/qutebrowser.spec +++ b/misc/qutebrowser.spec @@ -82,7 +82,7 @@ def get_data_files(): def get_hidden_imports(): - imports = [] if "PYINSTALLER_QT6" in os.environ else ['PyQt5.QtOpenGL'] + imports = ["PyQt5.QtOpenGL"] if "PYINSTALLER_QT5" in os.environ else [] for info in loader.walk_components(): imports.append(info.name) return imports diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index ed13e40eb..01815b647 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -5,15 +5,15 @@ build==0.10.0 bump2version==1.0.1 certifi==2023.5.7 cffi==1.15.1 -charset-normalizer==3.1.0 -cryptography==41.0.1 +charset-normalizer==3.2.0 +cryptography==41.0.2 docutils==0.20.1 github3.py==4.0.1 hunter==3.6.1 idna==3.4 -importlib-metadata==6.7.0 -importlib-resources==5.12.0 -jaraco.classes==3.2.3 +importlib-metadata==6.8.0 +importlib-resources==6.0.0 +jaraco.classes==3.3.0 jeepney==0.8.0 keyring==24.2.0 manhole==1.8.0 @@ -25,7 +25,7 @@ pkginfo==1.9.6 ply==3.11 pycparser==2.21 Pygments==2.15.1 -PyJWT==2.7.0 +PyJWT==2.8.0 Pympler==1.0.1 pyproject_hooks==1.0.0 PyQt-builder==1.15.1 @@ -42,6 +42,6 @@ tomli==2.0.1 twine==4.0.2 typing_extensions==4.7.1 uritemplate==4.1.1 -# urllib3==2.0.3 +# urllib3==2.0.4 webencodings==0.5.1 -zipp==3.15.0 +zipp==3.16.2 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index f00809320..685542224 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -2,9 +2,9 @@ attrs==23.1.0 flake8==6.0.0 -flake8-bugbear==23.6.5 +flake8-bugbear==23.7.10 flake8-builtins==2.1.0 -flake8-comprehensions==3.13.0 +flake8-comprehensions==3.14.0 flake8-debugger==4.1.2 flake8-deprecated==2.0.1 flake8-docstrings==1.7.0 @@ -12,7 +12,7 @@ flake8-future-import==0.4.7 flake8-plugin-utils==1.3.3 flake8-pytest-style==1.7.2 flake8-string-format==0.3.0 -flake8-tidy-imports==4.9.0 +flake8-tidy-imports==4.10.0 flake8-tuple==0.4.1 mccabe==0.7.0 pep8-naming==0.13.3 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 82b80bc01..24feda7d6 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,22 +1,21 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py chardet==5.1.0 -diff-cover==7.6.0 -importlib-resources==5.12.0 +diff-cover==7.7.0 +importlib-resources==6.0.0 Jinja2==3.1.2 -lxml==4.9.2 +lxml==4.9.3 MarkupSafe==2.1.3 mypy==1.4.1 mypy-extensions==1.0.0 pluggy==1.2.0 Pygments==2.15.1 PyQt5-stubs==5.15.6.0 -PyQt6-stubs @ git+https://github.com/python-qt-tools/PyQt6-stubs.git@f623a641cd5cdff53342177e4fbbf9cae8172336 tomli==2.0.1 types-colorama==0.4.15.11 types-docutils==0.20.0.1 types-Pygments==2.15.0.1 types-PyYAML==6.0.12.10 -types-setuptools==68.0.0.0 +types-setuptools==68.0.0.2 typing_extensions==4.7.1 -zipp==3.15.0 +zipp==3.16.2 diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw index 487d30ca6..027f4fef6 100644 --- a/misc/requirements/requirements-mypy.txt-raw +++ b/misc/requirements/requirements-mypy.txt-raw @@ -3,7 +3,6 @@ lxml # For HTML reports diff-cover PyQt5-stubs -git+https://github.com/python-qt-tools/PyQt6-stubs.git types-PyYAML types-colorama types-Pygments diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 7934bec0d..759c6f11f 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -2,4 +2,4 @@ altgraph==0.17.3 pyinstaller==5.13.0 -pyinstaller-hooks-contrib==2023.4 +pyinstaller-hooks-contrib==2023.5 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index c210631e3..e7a24df51 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,10 +1,10 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==2.15.5 +astroid==2.15.6 certifi==2023.5.7 cffi==1.15.1 -charset-normalizer==3.1.0 -cryptography==41.0.1 +charset-normalizer==3.2.0 +cryptography==41.0.2 dill==0.3.6 github3.py==4.0.1 idna==3.4 @@ -12,9 +12,9 @@ isort==5.12.0 lazy-object-proxy==1.9.0 mccabe==0.7.0 pefile==2023.2.7 -platformdirs==3.8.0 +platformdirs==3.9.1 pycparser==2.21 -PyJWT==2.7.0 +PyJWT==2.8.0 pylint==2.17.4 python-dateutil==2.8.2 ./scripts/dev/pylint_checkers @@ -24,5 +24,5 @@ tomli==2.0.1 tomlkit==0.11.8 typing_extensions==4.7.1 uritemplate==4.1.1 -# urllib3==2.0.3 +# urllib3==2.0.4 wrapt==1.15.0 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 029fb4a6b..26f81ab23 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.15.9 -PyQt5-Qt5==5.15.2 -PyQt5-sip==12.12.1 -PyQtWebEngine==5.15.6 -PyQtWebEngine-Qt5==5.15.2 +PyQt6==6.5.1 +PyQt6-Qt6==6.5.1 +PyQt6-sip==13.5.1 +PyQt6-WebEngine==6.5.0 +PyQt6-WebEngine-Qt6==6.5.1 diff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw index 9c6afbf16..68a5db685 100644 --- a/misc/requirements/requirements-pyqt.txt-raw +++ b/misc/requirements/requirements-pyqt.txt-raw @@ -1,2 +1,4 @@ -PyQt5 -PyQtWebEngine +PyQt6 +PyQt6-Qt6 +PyQt6-WebEngine +PyQt6-WebEngine-Qt6 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index e0d7fe585..6aa40fd97 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -2,7 +2,7 @@ build==0.10.0 certifi==2023.5.7 -charset-normalizer==3.1.0 +charset-normalizer==3.2.0 docutils==0.20.1 idna==3.4 packaging==23.1 @@ -11,5 +11,5 @@ pyproject_hooks==1.0.0 pyroma==4.2 requests==2.31.0 tomli==2.0.1 -trove-classifiers==2023.5.24 -urllib3==2.0.3 +trove-classifiers==2023.7.6 +urllib3==2.0.4 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index e92e41b8c..a9cafa9d3 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -3,11 +3,11 @@ alabaster==0.7.13 Babel==2.12.1 certifi==2023.5.7 -charset-normalizer==3.1.0 +charset-normalizer==3.2.0 docutils==0.20.1 idna==3.4 imagesize==1.4.1 -importlib-metadata==6.7.0 +importlib-metadata==6.8.0 Jinja2==3.1.2 MarkupSafe==2.1.3 packaging==23.1 @@ -22,5 +22,5 @@ sphinxcontrib-htmlhelp==2.0.1 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 -urllib3==2.0.3 -zipp==3.15.0 +urllib3==2.0.4 +zipp==3.16.2 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 28b9e6f5d..137d01ca5 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -4,18 +4,18 @@ attrs==23.1.0 beautifulsoup4==4.12.2 blinker==1.6.2 certifi==2023.5.7 -charset-normalizer==3.1.0 +charset-normalizer==3.2.0 cheroot==10.0.0 -click==8.1.3 +click==8.1.6 coverage==7.2.7 -exceptiongroup==1.1.1 -execnet==1.9.0 +exceptiongroup==1.1.2 +execnet==2.0.2 filelock==3.12.2 Flask==2.3.2 hunter==3.6.1 -hypothesis==6.80.0 +hypothesis==6.82.0 idna==3.4 -importlib-metadata==6.7.0 +importlib-metadata==6.8.0 iniconfig==2.0.0 itsdangerous==2.1.2 jaraco.functools==3.8.0 @@ -26,7 +26,7 @@ manhole==1.8.0 more-itertools==9.1.0 packaging==23.1 parse==1.19.1 -parse-type==0.6.1 +parse-type==0.6.2 pluggy==1.2.0 py-cpuinfo==9.0.0 Pygments==2.15.1 @@ -38,7 +38,7 @@ pytest-instafail==0.5.0 pytest-mock==3.11.1 pytest-qt==4.2.0 pytest-repeat==0.9.1 -pytest-rerunfailures==11.1.2 +pytest-rerunfailures==12.0 pytest-xdist==3.3.1 pytest-xvfb==3.0.0 PyVirtualDisplay==3.0 @@ -51,7 +51,7 @@ tldextract==3.4.4 toml==0.10.2 tomli==2.0.1 typing_extensions==4.7.1 -urllib3==2.0.3 +urllib3==2.0.4 vulture==2.7 Werkzeug==2.3.6 -zipp==3.15.0 +zipp==3.16.2 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index e68e79d46..a522764bd 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -3,15 +3,15 @@ cachetools==5.3.1 chardet==5.1.0 colorama==0.4.6 -distlib==0.3.6 +distlib==0.3.7 filelock==3.12.2 packaging==23.1 -pip==23.1.2 -platformdirs==3.8.0 +pip==23.2 +platformdirs==3.9.1 pluggy==1.2.0 -pyproject-api==1.5.2 +pyproject-api==1.5.3 setuptools==68.0.0 tomli==2.0.1 -tox==4.6.3 -virtualenv==20.23.1 +tox==4.6.4 +virtualenv==20.24.1 wheel==0.40.0 diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt index 718012a4e..a35c0ff58 100644 --- a/misc/requirements/requirements-yamllint.txt +++ b/misc/requirements/requirements-yamllint.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py pathspec==0.11.1 -PyYAML==6.0 +PyYAML==6.0.1 yamllint==1.32.0 diff --git a/misc/userscripts/add-nextcloud-bookmarks b/misc/userscripts/add-nextcloud-bookmarks index 86f4f5bc7..2a480ccff 100755 --- a/misc/userscripts/add-nextcloud-bookmarks +++ b/misc/userscripts/add-nextcloud-bookmarks @@ -41,7 +41,7 @@ from json import dumps from os import environ, path from sys import argv, exit -from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit +from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit from requests import get, post from requests.auth import HTTPBasicAuth @@ -54,7 +54,7 @@ def get_text(name, info): None, "add-nextcloud-bookmarks userscript", "Please enter {}".format(info), - QLineEdit.Password, + QLineEdit.EchoMode.Password, ) else: text, ok = QInputDialog.getText( diff --git a/misc/userscripts/add-nextcloud-cookbook b/misc/userscripts/add-nextcloud-cookbook index 3952bb16f..151090785 100755 --- a/misc/userscripts/add-nextcloud-cookbook +++ b/misc/userscripts/add-nextcloud-cookbook @@ -37,7 +37,7 @@ import configparser from os import environ, path from sys import argv, exit -from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit +from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit from requests import post from requests.auth import HTTPBasicAuth @@ -50,7 +50,7 @@ def get_text(name, info): None, "add-nextcloud-cookbook userscript", "Please enter {}".format(info), - QLineEdit.Password, + QLineEdit.EchoMode.Password, ) else: text, ok = QInputDialog.getText( diff --git a/misc/userscripts/qute-keepass b/misc/userscripts/qute-keepass index 285377ffc..f88493d8e 100755 --- a/misc/userscripts/qute-keepass +++ b/misc/userscripts/qute-keepass @@ -42,7 +42,7 @@ you do not do this, you will get 'element not editable' errors. If keepass takes a while to open the DB, you might want to consider reducing the number of transform rounds in your database settings. -Dependencies: pykeepass (in python3), PyQt5. Without pykeepass, you will get an +Dependencies: pykeepass (in python3), PyQt6. Without pykeepass, you will get an exit code of 100. ********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!****************** @@ -64,8 +64,8 @@ import shlex import subprocess import sys -from PyQt5.QtCore import QUrl -from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit +from PyQt6.QtCore import QUrl +from PyQt6.QtWidgets import QApplication, QInputDialog, QLineEdit try: import pykeepass @@ -152,7 +152,7 @@ def get_password(): text, ok = QInputDialog.getText( None, "KeePass DB Password", "Please enter your KeePass Master Password", - QLineEdit.Password) + QLineEdit.EchoMode.Password) if not ok: stderr('Password Prompt Rejected.') sys.exit(ExitCodes.USER_QUIT) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index bb2ff56e7..f40c31a50 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -42,7 +42,7 @@ import tempfile import pathlib import datetime import argparse -from typing import Iterable, Optional +from typing import Iterable, Optional, List, Tuple from qutebrowser.qt import machinery from qutebrowser.qt.widgets import QApplication, QWidget @@ -340,7 +340,7 @@ def _open_special_pages(args): tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') - pages = [ + pages: List[Tuple[str, bool, str]] = [ # state, condition, URL ('quickstart-done', True, @@ -367,6 +367,14 @@ def _open_special_pages(args): os.environ.get("QTWEBENGINE_DISABLE_SANDBOX") == "1" ), 'qute://warning/sandboxing'), + + ('qt5-warning-shown', + ( + machinery.IS_QT5 and + machinery.INFO.reason == machinery.SelectionReason.auto and + objects.backend != usertypes.Backend.QtWebKit + ), + 'qute://warning/qt5'), ] if 'quickstart-done' not in general_sect: diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 47cba5922..6f68956f8 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -286,10 +286,16 @@ class AbstractPrinting(QObject): """ raise NotImplementedError + def _do_print(self) -> None: + assert self._dialog is not None + printer = self._dialog.printer() + assert printer is not None + self.to_printer(printer) + def show_dialog(self) -> None: """Print with a QPrintDialog.""" - self._dialog = dialog = QPrintDialog(self._tab) - self._dialog.open(lambda: self.to_printer(dialog.printer())) + self._dialog = QPrintDialog(self._tab) + self._dialog.open(self._do_print) # Gets cleaned up in on_printing_finished diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3b38c44c0..410b844a0 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1597,8 +1597,7 @@ class CommandDispatcher: def _search_navigation_cb(self, result): """Callback called from :search-prev/next.""" if result == browsertab.SearchNavigationResult.not_found: - # FIXME check if this actually can happen... - message.warning("Search result vanished...") + self._search_cb(found=False, text=self._tabbed_browser.search_text) return elif result == browsertab.SearchNavigationResult.found: return diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index f4790bc9f..02bba7a41 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -77,9 +77,10 @@ class DownloadView(QListView): self.clicked.connect(self.on_clicked) def __repr__(self): - model = self.model() + model = qtutils.add_optional(self.model()) + count: Union[int, str] if model is None: - count = 'None' # type: ignore[unreachable] + count = 'None' else: count = model.rowCount() return utils.get_repr(self, count=count) @@ -173,9 +174,12 @@ class DownloadView(QListView): assert name is not None assert handler is not None action = self._menu.addAction(name) + assert action is not None action.triggered.connect(handler) if actions: - self._menu.popup(self.viewport().mapToGlobal(point)) + viewport = self.viewport() + assert viewport is not None + self._menu.popup(viewport.mapToGlobal(point)) def minimumSizeHint(self): """Override minimumSizeHint so the size is correct in a layout.""" diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index c0b23040c..a83621ae0 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -23,8 +23,9 @@ import contextlib import pathlib from typing import cast, Mapping, MutableSequence, Optional +from qutebrowser.qt import machinery from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, pyqtSignal -from qutebrowser.qt.widgets import QProgressDialog, QApplication +from qutebrowser.qt.widgets import QProgressDialog, QApplication, QPushButton from qutebrowser.config import config from qutebrowser.api import cmdutils @@ -54,7 +55,13 @@ class HistoryProgress: self._progress.setMaximum(0) # unknown self._progress.setMinimumDuration(0) self._progress.setLabelText(text) - self._progress.setCancelButton(None) + + no_button = None + if machinery.IS_QT6: + # FIXME:mypy PyQt6 stubs issue + no_button = cast(QPushButton, None) + + self._progress.setCancelButton(no_button) self._progress.setAutoClose(False) self._progress.show() QApplication.processEvents() diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index a2ce67750..ed0cae56f 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -28,7 +28,7 @@ from qutebrowser.qt.gui import QCloseEvent from qutebrowser.browser import eventfilter from qutebrowser.config import configfiles, config -from qutebrowser.utils import log, usertypes +from qutebrowser.utils import log, usertypes, qtutils from qutebrowser.keyinput import modeman from qutebrowser.misc import miscwidgets @@ -70,8 +70,9 @@ class _EventFilter(QObject): clicked = pyqtSignal() - def eventFilter(self, _obj: QObject, event: QEvent) -> bool: + def eventFilter(self, _obj: Optional[QObject], event: Optional[QEvent]) -> bool: """Translate mouse presses to a clicked signal.""" + assert event is not None if event.type() == QEvent.Type.MouseButtonPress: self.clicked.emit() return False @@ -162,7 +163,7 @@ class AbstractWebInspector(QWidget): self.shutdown() return elif position == Position.window: - self.setParent(None) # type: ignore[call-overload] + self.setParent(qtutils.QT_NONE) self._load_state_geometry() else: self._splitter.set_inspector(self, position) @@ -195,7 +196,7 @@ class AbstractWebInspector(QWidget): if not ok: log.init.warning("Error while loading geometry.") - def closeEvent(self, _e: QCloseEvent) -> None: + def closeEvent(self, _e: Optional[QCloseEvent]) -> None: """Save the geometry when closed.""" data = self._widget.saveGeometry().data() geom = base64.b64encode(data).decode('ASCII') diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index c66b6bc03..162e1c5d0 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -28,7 +28,7 @@ from qutebrowser.qt.network import (QNetworkProxy, QNetworkRequest, QHostInfo, QHostAddress) from qutebrowser.qt.qml import QJSEngine, QJSValue -from qutebrowser.utils import log, utils, qtutils, resources, urlutils +from qutebrowser.utils import log, qtlog, utils, qtutils, resources, urlutils class ParseProxyError(Exception): @@ -65,7 +65,8 @@ def _js_slot(*args): return self._error_con.callAsConstructor([e]) # pylint: enable=protected-access - deco = pyqtSlot(*args, result=QJSValue) + # FIXME:mypy PyQt6 stubs issue, passing type should work too + deco = pyqtSlot(*args, result="QJSValue") return deco(new_method) return _decorator @@ -257,7 +258,7 @@ class PACFetcher(QObject): url.setScheme(url.scheme()[len(pac_prefix):]) self._pac_url = url - with log.disable_qt_msghandler(): + with qtlog.disable_qt_msghandler(): # WORKAROUND for a hang when messages are printed, see our # NetworkAccessManager subclass for details. self._manager: Optional[QNetworkAccessManager] = QNetworkAccessManager() @@ -276,6 +277,7 @@ class PACFetcher(QObject): """Fetch the proxy from the remote URL.""" assert self._manager is not None self._reply = self._manager.get(QNetworkRequest(self._pac_url)) + assert self._reply is not None self._reply.finished.connect(self._finish) @pyqtSlot() diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index 714823d2c..53aaac38c 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -21,7 +21,7 @@ from qutebrowser.qt.core import QUrl, pyqtSlot from qutebrowser.qt.network import QNetworkProxy, QNetworkProxyFactory from qutebrowser.config import config, configtypes -from qutebrowser.utils import message, usertypes, urlutils, utils +from qutebrowser.utils import message, usertypes, urlutils, utils, qtutils from qutebrowser.misc import objects from qutebrowser.browser.network import pac @@ -51,7 +51,7 @@ def _warn_for_pac(): @pyqtSlot() def shutdown(): QNetworkProxyFactory.setApplicationProxyFactory( - None) # type: ignore[arg-type] + qtutils.QT_NONE) class ProxyFactory(QNetworkProxyFactory): diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index cd4a75351..0b20b3785 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -29,7 +29,7 @@ from qutebrowser.qt.widgets import QApplication from qutebrowser.qt.network import QNetworkRequest, QNetworkReply, QNetworkAccessManager from qutebrowser.config import config, websettings -from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg +from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg, qtlog from qutebrowser.misc import quitter from qutebrowser.browser import downloads from qutebrowser.browser.webkit import http @@ -121,7 +121,7 @@ class DownloadItem(downloads.AbstractDownloadItem): self._reply.errorOccurred.disconnect() self._reply.readyRead.disconnect() - with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal ' + with qtlog.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal ' 'problem, this method must only be called ' 'once.'): # See https://codereview.qt-project.org/#/c/107863/ diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 25834670b..0073f9bd2 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -22,6 +22,7 @@ Module attributes: _HANDLERS: The handlers registered via decorators. """ +import sys import html import json import os @@ -583,6 +584,12 @@ def qute_warning(url: QUrl) -> _HandlerRet: elif path == '/sandboxing': src = jinja.render('warning-sandboxing.html', title='Qt 6 macOS sandboxing warning') + elif path == '/qt5': + is_venv = hasattr(sys, 'real_prefix') or sys.base_prefix != sys.prefix + src = jinja.render('warning-qt5.html', + title='Switch to Qt 6', + is_venv=is_venv, + prefix=sys.prefix) else: raise NotFoundError("Invalid warning page {}".format(path)) return 'text/html', src diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 75acb3252..f56db3a65 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -33,9 +33,8 @@ class WebEngineRequest(interceptors.Request): """QtWebEngine-specific request interceptor functionality.""" _WHITELISTED_REQUEST_METHODS = { - # FIXME:mypy PyQt6-stubs issue? - QByteArray(b'GET'), # type: ignore[call-overload,unused-ignore] - QByteArray(b'HEAD'), # type: ignore[call-overload,unused-ignore] + QByteArray(b'GET'), + QByteArray(b'HEAD'), } def __init__(self, *args, webengine_info, **kwargs): diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py index d140a8c61..d101c616c 100644 --- a/qutebrowser/browser/webengine/notification.py +++ b/qutebrowser/browser/webengine/notification.py @@ -1108,9 +1108,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter): if padding and self._quirks.no_padded_images: return None - bits = qimage.constBits().asstring(size) - # FIXME:mypy PyQt6-stubs issue - image_data.add(QByteArray(bits)) # type: ignore[call-overload,unused-ignore] + bits_ptr = qimage.constBits() + assert bits_ptr is not None + bits = bits_ptr.asstring(size) + image_data.add(QByteArray(bits)) image_data.endStructure() return image_data diff --git a/qutebrowser/browser/webengine/tabhistory.py b/qutebrowser/browser/webengine/tabhistory.py index 30f3facdb..2848142ef 100644 --- a/qutebrowser/browser/webengine/tabhistory.py +++ b/qutebrowser/browser/webengine/tabhistory.py @@ -153,6 +153,8 @@ def serialize(items): for item in items: _serialize_item(item, stream) - stream.device().reset() + dev = stream.device() + assert dev is not None + dev.reset() qtutils.check_qdatastream(stream) return stream, data, cur_user_data diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 4a09c81fb..010b00975 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -25,8 +25,7 @@ from qutebrowser.qt.webenginecore import (QWebEngineUrlSchemeHandler, from qutebrowser.browser import qutescheme from qutebrowser.utils import log, qtutils -# FIXME:mypy PyQt6-stubs issue? -_QUTE = QByteArray(b'qute') # type: ignore[call-overload,unused-ignore] +_QUTE = QByteArray(b'qute') class QuteSchemeHandler(QWebEngineUrlSchemeHandler): diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index e55d75ecd..c2957181b 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -400,7 +400,8 @@ class WebEngineCaret(browsertab.AbstractCaret): # https://bugreports.qt.io/browse/QTBUG-53134 # Even on Qt 5.10 selectedText() seems to work poorly, see # https://github.com/qutebrowser/qutebrowser/issues/3523 - # FIXME:qt6 Reevaluate? + # With Qt 6.2-6.5, there still seem to be issues (especially with + # multi-line text) self._tab.run_js_async(javascript.assemble('caret', 'getSelection'), callback) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 9d58c0379..ade008143 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -54,7 +54,9 @@ class WebEngineView(QWebEngineView): self._win_id = win_id self._tabdata = tabdata - theme_color = self.style().standardPalette().color(QPalette.ColorRole.Base) + style = self.style() + assert style is not None + theme_color = style.standardPalette().color(QPalette.ColorRole.Base) if private: assert webenginesettings.private_profile is not None profile = webenginesettings.private_profile diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 91e609456..4c1c767ec 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -28,7 +28,7 @@ from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkReply, QSslCo from qutebrowser.config import config from qutebrowser.utils import (message, log, usertypes, utils, objreg, - urlutils, debug) + urlutils, debug, qtlog) from qutebrowser.browser import shared from qutebrowser.browser.network import proxy as proxymod from qutebrowser.extensions import interceptors @@ -156,7 +156,7 @@ class NetworkManager(QNetworkAccessManager): def __init__(self, *, win_id, tab_id, private, parent=None): log.init.debug("Initializing NetworkManager") - with log.disable_qt_msghandler(): + with qtlog.disable_qt_msghandler(): # WORKAROUND for a hang when a message is printed - See: # https://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html # diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index ef3e3bea5..8bf5031b1 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -15,16 +15,15 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """QtWebKit specific part of the web element API.""" from typing import cast, TYPE_CHECKING, Iterator, List, Optional, Set from qutebrowser.qt.core import QRect, Qt +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebElement, QWebSettings from qutebrowser.qt.webkitwidgets import QWebFrame +# pylint: enable=no-name-in-module from qutebrowser.config import config from qutebrowser.utils import log, utils, javascript, usertypes diff --git a/qutebrowser/browser/webkit/webkithistory.py b/qutebrowser/browser/webkit/webkithistory.py index aea648361..d89c705e6 100644 --- a/qutebrowser/browser/webkit/webkithistory.py +++ b/qutebrowser/browser/webkit/webkithistory.py @@ -15,14 +15,13 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """QtWebKit specific part of history.""" import functools +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebHistoryInterface +# pylint: enable=no-name-in-module from qutebrowser.utils import debug from qutebrowser.misc import debugcachestats diff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py index cb9cb5615..c181435d1 100644 --- a/qutebrowser/browser/webkit/webkitinspector.py +++ b/qutebrowser/browser/webkit/webkitinspector.py @@ -15,13 +15,13 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module """Customized QWebInspector for QtWebKit.""" +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebSettings from qutebrowser.qt.webkitwidgets import QWebInspector, QWebPage +# pylint: enable=no-name-in-module from qutebrowser.qt.widgets import QWidget from qutebrowser.browser import inspector diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index bd65be65b..a20811bae 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """Bridge from QWebSettings to our own settings. Module attributes: @@ -30,8 +27,10 @@ import os.path from qutebrowser.qt.core import QUrl from qutebrowser.qt.gui import QFont +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebSettings from qutebrowser.qt.webkitwidgets import QWebPage +# pylint: enable=no-name-in-module from qutebrowser.config import config, websettings from qutebrowser.config.websettings import AttributeInfo as Attr diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index a756e1a3d..e0483a23a 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module """Wrapper over our (QtWebKit) WebView.""" @@ -28,8 +26,10 @@ from typing import cast, Iterable, Optional from qutebrowser.qt.core import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize from qutebrowser.qt.gui import QIcon from qutebrowser.qt.widgets import QWidget +# pylint: disable=no-name-in-module from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame from qutebrowser.qt.webkit import QWebSettings, QWebHistory, QWebElement +# pylint: enable=no-name-in-module from qutebrowser.qt.printsupport import QPrinter from qutebrowser.browser import browsertab, shared diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 27429f331..b3b1b7ceb 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """The main browser widgets.""" import html @@ -28,7 +25,9 @@ from qutebrowser.qt.gui import QDesktopServices from qutebrowser.qt.network import QNetworkReply, QNetworkRequest from qutebrowser.qt.widgets import QFileDialog from qutebrowser.qt.printsupport import QPrintDialog +# pylint: disable=no-name-in-module from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame +# pylint: enable=no-name-in-module from qutebrowser.config import websettings, config from qutebrowser.browser import pdfjs, shared, downloads, greasemonkey diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 7a08a0736..831b2b689 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -15,14 +15,13 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """The main browser widgets.""" from qutebrowser.qt.core import pyqtSignal, Qt +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebSettings from qutebrowser.qt.webkitwidgets import QWebView, QWebPage +# pylint: enable=no-name-in-module from qutebrowser.config import config, stylesheet from qutebrowser.keyinput import modeman diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index e497e1204..cc5859ca6 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -291,6 +291,7 @@ class CompletionItemDelegate(QStyledItemDelegate): self._opt = QStyleOptionViewItem(option) self.initStyleOption(self._opt, index) self._style = self._opt.widget.style() + assert self._style is not None self._get_textdoc(index) assert self._doc is not None diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 01527e763..665757e89 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -156,6 +156,15 @@ class CompletionView(QTreeView): assert isinstance(model, completionmodel.CompletionModel), model return model + def _selection_model(self) -> QItemSelectionModel: + """Get the current selection model. + + Ensures the model is not None. + """ + model = self.selectionModel() + assert model is not None + return model + @pyqtSlot(str) def _on_config_changed(self, option): if option in ['completion.height', 'completion.shrink']: @@ -169,7 +178,9 @@ class CompletionView(QTreeView): column_widths = self._model().column_widths pixel_widths = [(width * perc // 100) for perc in column_widths] - delta = self.verticalScrollBar().sizeHint().width() + bar = self.verticalScrollBar() + assert bar is not None + delta = bar.sizeHint().width() for i, width in reversed(list(enumerate(pixel_widths))): if width > delta: pixel_widths[i] -= delta @@ -191,7 +202,7 @@ class CompletionView(QTreeView): A QModelIndex. """ model = self._model() - idx = self.selectionModel().currentIndex() + idx = self._selection_model().currentIndex() if not idx.isValid(): # No item selected yet if upwards: @@ -223,7 +234,7 @@ class CompletionView(QTreeView): Return: A QModelIndex. """ - old_idx = self.selectionModel().currentIndex() + old_idx = self._selection_model().currentIndex() idx = old_idx model = self._model() @@ -267,7 +278,7 @@ class CompletionView(QTreeView): Return: A QModelIndex. """ - idx = self.selectionModel().currentIndex() + idx = self._selection_model().currentIndex() model = self._model() if not idx.isValid(): return self._next_idx(upwards).sibling(0, 0) @@ -323,7 +334,7 @@ class CompletionView(QTreeView): if not self._active: return - selmodel = self.selectionModel() + selmodel = self._selection_model() indices = { 'next': lambda: self._next_idx(upwards=False), 'prev': lambda: self._next_idx(upwards=True), @@ -363,9 +374,10 @@ class CompletionView(QTreeView): Args: model: The model to use. """ - if self.model() is not None and model is not self.model(): - self.model().deleteLater() - self.selectionModel().deleteLater() + old_model = self.model() + if old_model is not None and model is not old_model: + old_model.deleteLater() + self._selection_model().deleteLater() self.setModel(model) @@ -395,7 +407,7 @@ class CompletionView(QTreeView): self.pattern = pattern with debug.log_time(log.completion, 'Set pattern {}'.format(pattern)): self._model().set_pattern(pattern) - self.selectionModel().clear() + self._selection_model().clear() self._maybe_update_geometry() self._maybe_show() @@ -415,7 +427,7 @@ class CompletionView(QTreeView): def on_clear_completion_selection(self): """Clear the selection model when an item is activated.""" self.hide() - selmod = self.selectionModel() + selmod = self._selection_model() if selmod is not None: selmod.clearSelection() selmod.clearCurrentIndex() @@ -426,14 +438,18 @@ class CompletionView(QTreeView): confheight = str(config.val.completion.height) if confheight.endswith('%'): perc = int(confheight.rstrip('%')) - height = self.window().height() * perc // 100 + window = self.window() + assert window is not None + height = window.height() * perc // 100 else: height = int(confheight) # Shrink to content size if needed and shrinking is enabled if config.val.completion.shrink: + bar = self.horizontalScrollBar() + assert bar is not None contents_height = ( self.viewportSizeHint().height() + - self.horizontalScrollBar().sizeHint().height()) + bar.sizeHint().height()) if contents_height <= height: height = contents_height # The width isn't really relevant as we're expanding anyways. diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 2a13133ae..ac593cfae 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -108,7 +108,9 @@ class StateConfig(configparser.ConfigParser): for sect, key in deleted_keys: self[sect].pop(key, None) - self['general']['qt_version'] = qVersion() + qt_version = qVersion() + assert qt_version is not None + self['general']['qt_version'] = qt_version self['general']['qtwe_version'] = self._qtwe_version_str() self['general']['chromium_version'] = self._chromium_version_str() self['general']['version'] = qutebrowser.__version__ diff --git a/qutebrowser/html/warning-qt5.html b/qutebrowser/html/warning-qt5.html new file mode 100644 index 000000000..17af2f72c --- /dev/null +++ b/qutebrowser/html/warning-qt5.html @@ -0,0 +1,28 @@ +{% extends "styled.html" %} + +{% block content %} +<h1>{{ title }}</h1> +<span class="note">Note this warning will only appear once. Use <span class="mono">:open +qute://warning/qt5</span> to show it again at a later time.</span> + +<p> + qutebrowser <b>now supports Qt 6</b>. +</p> +<p> + However, in your environment, <b>Qt 6 is not installed</b>. Thus, qutebrowser is still using Qt 5 instead. + + Qt 5.15 based on a very old Chromium version (83 or 87, from mid/late 2020). +</p> +{% if is_venv %} +<p> + You are using a virtualenv. If you want to use Qt 6, you need to create a new + virtualenv with PyQt6 installed. + + If using <span class="mono">mkvenv.py</span>, <b>rerun the script</b> to create a + new virtualenv with Qt 6. +</p> +{% endif %} +<p> + <span class="note">Python installation prefix: <span class="mono">{{ prefix }}</span></span> +</p> +{% endblock %} diff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py index 007be6d15..40581b3c1 100644 --- a/qutebrowser/keyinput/eventfilter.py +++ b/qutebrowser/keyinput/eventfilter.py @@ -17,9 +17,8 @@ """Global Qt event filter which dispatches key events.""" -from typing import cast +from typing import cast, Optional -from qutebrowser.qt import machinery from qutebrowser.qt.core import pyqtSlot, QObject, QEvent from qutebrowser.qt.gui import QKeyEvent, QWindow @@ -76,7 +75,7 @@ class EventFilter(QObject): # No window available yet, or not a MainWindow return False - def eventFilter(self, obj: QObject, event: QEvent) -> bool: + def eventFilter(self, obj: Optional[QObject], event: Optional[QEvent]) -> bool: """Handle an event. Args: @@ -86,9 +85,8 @@ class EventFilter(QObject): Return: True if the event should be filtered, False if it's passed through. """ + assert event is not None ev_type = event.type() - if machinery.IS_QT6: - ev_type = cast(QEvent.Type, ev_type) if self._log_qt_events: try: diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index e2a15b2c0..10f4d5378 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -37,9 +37,7 @@ from qutebrowser.qt import machinery from qutebrowser.qt.core import Qt, QEvent from qutebrowser.qt.gui import QKeySequence, QKeyEvent if machinery.IS_QT6: - # FIXME:qt6 (lint) how come pylint isn't picking this up with both backends - # installed? - from qutebrowser.qt.core import QKeyCombination # pylint: disable=no-name-in-module + from qutebrowser.qt.core import QKeyCombination else: QKeyCombination = None # QKeyCombination was added in Qt 6 @@ -349,7 +347,7 @@ def _unset_modifier_bits( https://github.com/python/cpython/issues/105497 """ if machinery.IS_QT5: - return cast(_ModifierType, modifiers & ~mask) + return Qt.KeyboardModifiers(modifiers & ~mask) # can lose type if it's 0 else: return Qt.KeyboardModifier(modifiers.value & ~mask.value) @@ -369,11 +367,14 @@ class KeyInfo: def __post_init__(self) -> None: """Run some validation on the key/modifier values.""" - # This is mainly useful while porting from Qt 5 to 6. - # FIXME:qt6 do we want to remove or keep this (and fix the remaining - # issues) when done? - # assert isinstance(self.key, Qt.Key), self.key - # assert isinstance(self.modifiers, Qt.KeyboardModifier), self.modifiers + # This changed with Qt 6, and e.g. to_qt() relies on this. + if machinery.IS_QT5: + modifier_classes = (Qt.KeyboardModifier, Qt.KeyboardModifiers) + elif machinery.IS_QT6: + modifier_classes = Qt.KeyboardModifier + assert isinstance(self.key, Qt.Key), self.key + assert isinstance(self.modifiers, modifier_classes), self.modifiers + _assert_plain_key(self.key) _assert_plain_modifier(self.modifiers) @@ -488,16 +489,7 @@ class KeyInfo: if machinery.IS_QT5: return int(self.key) | int(self.modifiers) else: - try: - # FIXME:qt6 We might want to consider only supporting KeyInfo to be - # instanciated with a real Qt.Key, not with ints. See __post_init__. - key = Qt.Key(self.key) - except ValueError as e: - # WORKAROUND for - # https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html - raise InvalidKeyError(e) - - return QKeyCombination(self.modifiers, key) + return QKeyCombination(self.modifiers, self.key) def with_stripped_modifiers(self, modifiers: Qt.KeyboardModifier) -> "KeyInfo": mods = _unset_modifier_bits(self.modifiers, modifiers) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index fe3650a2c..897318b66 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -474,11 +474,7 @@ class ModeManager(QObject): QEvent.Type.ShortcutOverride: functools.partial(self._handle_keypress, dry_run=True), } - ev_type = event.type() - if machinery.IS_QT6: - ev_type = cast(QEvent.Type, ev_type) - - handler = handlers[ev_type] + handler = handlers[event.type()] return handler(cast(QKeyEvent, event)) @cmdutils.register(instance='mode-manager', scope='window') diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 26a2ae886..17772b2ea 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -303,6 +303,7 @@ class PromptContainer(QWidget): item = self._layout.takeAt(0) if item is not None: widget = item.widget() + assert widget is not None log.prompt.debug("Deleting old prompt {}".format(widget)) widget.hide() widget.deleteLater() @@ -366,6 +367,7 @@ class PromptContainer(QWidget): item = self._layout.takeAt(0) if item is not None: widget = item.widget() + assert widget is not None log.prompt.debug("Deleting prompt {}".format(widget)) widget.hide() widget.deleteLater() @@ -780,6 +782,7 @@ class FilenamePrompt(_BasePrompt): # This duplicates some completion code, but I don't see a nicer way... assert which in ['prev', 'next'], which selmodel = self._file_view.selectionModel() + assert selmodel is not None parent = self._file_view.rootIndex() first_index = self._file_model.index(0, 0, parent) diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index 4332316a3..68bacd0b0 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -259,15 +259,19 @@ class Command(misc.CommandLineEdit): else: raise utils.Unreachable("setText got called with invalid text " "'{}'!".format(text)) + # FIXME:mypy PyQt6 stubs issue + if machinery.IS_QT6: + text = cast(str, text) super().setText(text) - def keyPressEvent(self, e: QKeyEvent) -> None: + def keyPressEvent(self, e: Optional[QKeyEvent]) -> None: """Override keyPressEvent to ignore Return key presses, and add Shift-Ins. If this widget is focused, we are in passthrough key mode, and Enter/Shift+Enter/etc. will cause QLineEdit to think it's finished without command_accept to be called. """ + assert e is not None if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing shift = cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier) else: diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index 54faf232d..7892b3e83 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -116,7 +116,9 @@ class UrlText(textbase.TextBase): if old_urltype != self._urltype: # We can avoid doing an unpolish here because the new style will # always override the old one. - self.style().polish(self) + style = self.style() + assert style is not None + style.polish(self) @pyqtSlot(usertypes.LoadStatus) def on_load_status_changed(self, status): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index da3392a7e..e597c9efe 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -249,7 +249,7 @@ class TabbedBrowser(QWidget): self.search_options: Mapping[str, Any] = {} self._local_marks: MutableMapping[QUrl, MutableMapping[str, QPoint]] = {} self._global_marks: MutableMapping[str, Tuple[QPoint, QUrl]] = {} - self.default_window_icon = self.widget.window().windowIcon() + self.default_window_icon = self._window().windowIcon() self.is_private = private self.tab_deque = TabDeque() config.instance.changed.connect(self._on_config_changed) @@ -301,10 +301,9 @@ class TabbedBrowser(QWidget): """ widgets = [] for i in range(self.widget.count()): - widget = self.widget.widget(i) + widget = qtutils.add_optional(self.widget.widget(i)) if widget is None: - log.webview.debug( # type: ignore[unreachable] - "Got None-widget in tabbedbrowser!") + log.webview.debug("Got None-widget in tabbedbrowser!") else: widgets.append(widget) return widgets @@ -330,7 +329,8 @@ class TabbedBrowser(QWidget): fields['id'] = self._win_id title = title_format.format(**fields) - self.widget.window().setWindowTitle(title) + + self._window().setWindowTitle(title) def _connect_tab_signals(self, tab): """Set up the needed signals for tab.""" @@ -396,6 +396,15 @@ class TabbedBrowser(QWidget): assert isinstance(tab, browsertab.AbstractTab), tab return tab + def _window(self) -> QWidget: + """Get the current window widget. + + Note: This asserts if there is no window. + """ + window = self.widget.window() + assert window is not None + return window + def _tab_by_idx(self, idx: int) -> Optional[browsertab.AbstractTab]: """Get a browser tab by index. @@ -662,11 +671,12 @@ class TabbedBrowser(QWidget): # Make sure the background tab has the correct initial size. # With a foreground tab, it's going to be resized correctly by the # layout anyways. - tab.resize(self.widget.currentWidget().size()) + current_widget = self._current_tab() + tab.resize(current_widget.size()) self.widget.tab_index_changed.emit(self.widget.currentIndex(), self.widget.count()) # Refocus webview in case we lost it by spawning a bg tab - self.widget.currentWidget().setFocus() + current_widget.setFocus() else: self.widget.setCurrentWidget(tab) @@ -739,7 +749,7 @@ class TabbedBrowser(QWidget): tab.data.keep_icon = False elif (config.cache['tabs.tabs_are_windows'] and tab.data.should_show_icon()): - self.widget.window().setWindowIcon(self.default_window_icon) + self._window().setWindowIcon(self.default_window_icon) @pyqtSlot() def _on_load_status_changed(self, tab): @@ -862,9 +872,10 @@ class TabbedBrowser(QWidget): @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): """Give focus to current tab if command mode was left.""" - widget = self.widget.currentWidget() + widget = qtutils.add_optional(self.widget.currentWidget()) if widget is None: - return # type: ignore[unreachable] + return + if mode in [usertypes.KeyMode.command] + modeman.PROMPT_MODES: log.modes.debug("Left status-input mode, focusing {!r}".format( widget)) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index fe9ce1e06..150c820a8 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -355,7 +355,9 @@ class TabWidget(QTabWidget): self.setTabIcon(idx, icon) if config.val.tabs.tabs_are_windows: - self.window().setWindowIcon(tab.icon()) + window = self.window() + assert window is not None + window.setWindowIcon(tab.icon()) def setTabIcon(self, idx: int, icon: QIcon) -> None: """Always show tab icons for pinned tabs in some circumstances.""" @@ -365,7 +367,9 @@ class TabWidget(QTabWidget): config.cache['tabs.pinned.shrink'] and not self.tab_bar().vertical and tab is not None and tab.data.pinned): - icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon) + style = self.style() + assert style is not None + icon = style.standardIcon(QStyle.StandardPixmap.SP_FileIcon) super().setTabIcon(idx, icon) @@ -809,6 +813,12 @@ class TabBarStyle(QProxyStyle): ICON_PADDING = 4 + def _base_style(self) -> QStyle: + """Get the base style.""" + style = self.baseStyle() + assert style is not None + return style + def _draw_indicator(self, layouts, opt, p): """Draw the tab indicator. @@ -836,7 +846,7 @@ class TabBarStyle(QProxyStyle): icon_state = (QIcon.State.On if opt.state & QStyle.StateFlag.State_Selected else QIcon.State.Off) icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state) - self.baseStyle().drawItemPixmap(p, layouts.icon, Qt.AlignmentFlag.AlignCenter, icon) + self._base_style().drawItemPixmap(p, layouts.icon, Qt.AlignmentFlag.AlignCenter, icon) def drawControl(self, element, opt, p, widget=None): """Override drawControl to draw odd tabs in a different color. @@ -853,7 +863,7 @@ class TabBarStyle(QProxyStyle): if element not in [QStyle.ControlElement.CE_TabBarTab, QStyle.ControlElement.CE_TabBarTabShape, QStyle.ControlElement.CE_TabBarTabLabel]: # Let the real style draw it. - self.baseStyle().drawControl(element, opt, p, widget) + self._base_style().drawControl(element, opt, p, widget) return layouts = self._tab_layout(opt) @@ -876,7 +886,7 @@ class TabBarStyle(QProxyStyle): self._draw_icon(layouts, opt, p) alignment = (config.cache['tabs.title.alignment'] | Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextHideMnemonic) - self.baseStyle().drawItemText( + self._base_style().drawItemText( p, layouts.text, int(alignment), @@ -906,7 +916,7 @@ class TabBarStyle(QProxyStyle): QStyle.PixelMetric.PM_TabBarScrollButtonWidth]: return 0 else: - return self.baseStyle().pixelMetric(metric, option, widget) + return self._base_style().pixelMetric(metric, option, widget) def subElementRect(self, sr, opt, widget=None): """Override subElementRect to use our own _tab_layout implementation. @@ -936,7 +946,7 @@ class TabBarStyle(QProxyStyle): # style differences... return QCommonStyle.subElementRect(self, sr, opt, widget) else: - return self.baseStyle().subElementRect(sr, opt, widget) + return self._base_style().subElementRect(sr, opt, widget) def _tab_layout(self, opt): """Compute the text/icon rect from the opt rect. @@ -983,7 +993,7 @@ class TabBarStyle(QProxyStyle): text_rect.adjust( icon_rect.width() + TabBarStyle.ICON_PADDING, 0, 0, 0) - text_rect = self.baseStyle().visualRect(opt.direction, opt.rect, text_rect) + text_rect = self._base_style().visualRect(opt.direction, opt.rect, text_rect) return Layouts(text=text_rect, icon=icon_rect, indicator=indicator_rect) @@ -1018,5 +1028,5 @@ class TabBarStyle(QProxyStyle): icon_top = text_rect.center().y() + 1 - tab_icon_size.height() // 2 icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size) - icon_rect = self.baseStyle().visualRect(opt.direction, opt.rect, icon_rect) + icon_rect = self._base_style().visualRect(opt.direction, opt.rect, icon_rect) return icon_rect diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 6d8d9916f..641798190 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -125,6 +125,7 @@ class ConsoleTextEdit(QTextEdit): self.moveCursor(QTextCursor.MoveOperation.End) self.insertPlainText(text) scrollbar = self.verticalScrollBar() + assert scrollbar is not None scrollbar.setValue(scrollbar.maximum()) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 416072ccb..04c92a529 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -631,8 +631,7 @@ class ReportErrorDialog(QDialog): hbox = QHBoxLayout() hbox.addStretch() btn = QPushButton("Close") - # FIXME:mypy PyQt6-stubs issue - btn.clicked.connect(self.close) # type: ignore[arg-type,unused-ignore] + btn.clicked.connect(self.close) hbox.addWidget(btn) vbox.addLayout(hbox) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index a0265d653..57e821784 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -298,7 +298,17 @@ def init_log(args): from qutebrowser.utils import log log.init_log(args) log.init.debug("Log initialized.") - log.init.debug(str(machinery.INFO)) + + +def init_qtlog(args): + """Initialize Qt logging. + + Args: + args: The argparse namespace. + """ + from qutebrowser.utils import log, qtlog + qtlog.init(args) + log.init.debug("Qt log initialized.") def check_optimize_flag(): @@ -333,16 +343,18 @@ def early_init(args): Args: args: The argparse namespace. """ + # Init logging as early as possible + init_log(args) # First we initialize the faulthandler as early as possible, so we # theoretically could catch segfaults occurring later during earlyinit. init_faulthandler() # Then we configure the selected Qt wrapper info = machinery.init(args) + # Init Qt logging after machinery is initialized + init_qtlog(args) # Here we check if QtCore is available, and if not, print a message to the # console or via Tk. check_qt_available(info) - # Init logging as early as possible - init_log(args) # Now we can be sure QtCore is available, so we can print dialogs on # errors, so people only using the GUI notice them as well. check_libraries() diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index a50849d29..ac7290ef4 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -27,7 +27,7 @@ from typing import Mapping, Sequence, Dict, Optional from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, QObject, QProcess, QProcessEnvironment, QByteArray, QUrl, Qt) -from qutebrowser.utils import message, log, utils, usertypes, version +from qutebrowser.utils import message, log, utils, usertypes, version, qtutils from qutebrowser.api import cmdutils, apitypes from qutebrowser.completion.models import miscmodels @@ -394,7 +394,7 @@ class GUIProcess(QObject): log.procs.debug("Starting process.") self._pre_start(cmd, args) self._proc.start( - self.resolved_cmd, # type: ignore[arg-type] + qtutils.remove_optional(self.resolved_cmd), args, ) self._post_start() diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py index 1dddddba7..45d491996 100644 --- a/qutebrowser/misc/httpclient.py +++ b/qutebrowser/misc/httpclient.py @@ -25,7 +25,7 @@ from qutebrowser.qt.core import pyqtSignal, QObject, QTimer from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkRequest, QNetworkReply) -from qutebrowser.utils import log +from qutebrowser.utils import qtlog class HTTPRequest(QNetworkRequest): @@ -59,7 +59,7 @@ class HTTPClient(QObject): def __init__(self, parent=None): super().__init__(parent) - with log.disable_qt_msghandler(): + with qtlog.disable_qt_msghandler(): # WORKAROUND for a hang when messages are printed, see our # NetworkAccessManager subclass for details. self._nam = QNetworkAccessManager(self) @@ -78,9 +78,7 @@ class HTTPClient(QObject): request = HTTPRequest(url) request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, 'application/x-www-form-urlencoded;charset=utf-8') - # FIXME:mypy PyQt6-stubs issue - reply = self._nam.post( # type: ignore[call-overload,unused-ignore] - request, encoded_data) + reply = self._nam.post(request, encoded_data) self._handle_reply(reply) def get(self, url): diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index fb1b1ac22..b809394f1 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -28,7 +28,7 @@ from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, Qt from qutebrowser.qt.network import QLocalSocket, QLocalServer, QAbstractSocket import qutebrowser -from qutebrowser.utils import log, usertypes, error, standarddir, utils, debug +from qutebrowser.utils import log, usertypes, error, standarddir, utils, debug, qtutils from qutebrowser.qt import sip @@ -259,10 +259,9 @@ class IPCServer(QObject): "still handling another one (0x{:x}).".format( id(self._socket))) return - socket = self._server.nextPendingConnection() + socket = qtutils.add_optional(self._server.nextPendingConnection()) if socket is None: - log.ipc.debug( # type: ignore[unreachable] - "No new connection to handle.") + log.ipc.debug("No new connection to handle.") return log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket))) self._socket = socket diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index ba33da775..1e90ac75a 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -25,7 +25,7 @@ from qutebrowser.qt.widgets import (QLineEdit, QWidget, QHBoxLayout, QLabel, from qutebrowser.qt.gui import QValidator, QPainter, QResizeEvent from qutebrowser.config import config, configfiles -from qutebrowser.utils import utils, log, usertypes, debug +from qutebrowser.utils import utils, log, usertypes, debug, qtutils from qutebrowser.misc import cmdhistory from qutebrowser.browser import inspector from qutebrowser.keyinput import keyutils, modeman @@ -185,7 +185,10 @@ class _FoldArrow(QWidget): elem = QStyle.PrimitiveElement.PE_IndicatorArrowRight else: elem = QStyle.PrimitiveElement.PE_IndicatorArrowDown - self.style().drawPrimitive(elem, opt, painter, self) + + style = self.style() + assert style is not None + style.drawPrimitive(elem, opt, painter, self) def minimumSizeHint(self): """Return a sensible size.""" @@ -241,10 +244,10 @@ class WrapperLayout(QLayout): if self._widget is None: return assert self._container is not None - self._widget.setParent(None) # type: ignore[call-overload] + self._widget.setParent(qtutils.QT_NONE) self._widget.deleteLater() self._widget = None - self._container.setFocusProxy(None) # type: ignore[arg-type] + self._container.setFocusProxy(qtutils.QT_NONE) class FullscreenNotification(QLabel): @@ -270,9 +273,17 @@ class FullscreenNotification(QLabel): self.resize(self.sizeHint()) if config.val.content.fullscreen.window: - geom = self.parentWidget().geometry() + parent = self.parentWidget() + assert parent is not None + geom = parent.geometry() else: - geom = self.window().windowHandle().screen().geometry() + window = self.window() + assert window is not None + handle = window.windowHandle() + assert handle is not None + screen = handle.screen() + assert screen is not None + geom = screen.geometry() self.move((geom.width() - self.sizeHint().width()) // 2, 30) def set_timeout(self, timeout): @@ -327,6 +338,8 @@ class InspectorSplitter(QSplitter): main_widget = self.widget(self._main_idx) inspector_widget = self.widget(self._inspector_idx) + assert main_widget is not None + assert inspector_widget is not None if not inspector_widget.isVisible(): raise inspector.Error("No inspector inside main window") @@ -439,8 +452,9 @@ class InspectorSplitter(QSplitter): self._preferred_size = sizes[self._inspector_idx] self._save_preferred_size() - def resizeEvent(self, e: QResizeEvent) -> None: + def resizeEvent(self, e: Optional[QResizeEvent]) -> None: """Window resize event.""" + assert e is not None super().resizeEvent(e) if self.count() == 2: self._adjust_size() diff --git a/qutebrowser/misc/nativeeventfilter.py b/qutebrowser/misc/nativeeventfilter.py index 4562ea82d..5fad3359c 100644 --- a/qutebrowser/misc/nativeeventfilter.py +++ b/qutebrowser/misc/nativeeventfilter.py @@ -20,7 +20,7 @@ This entire file is a giant WORKAROUND for https://bugreports.qt.io/browse/QTBUG-114334. """ -from typing import Tuple, Union, cast +from typing import Tuple, Union, cast, Optional import enum import ctypes import ctypes.util @@ -150,11 +150,12 @@ class NativeEventFilter(QAbstractNativeEventFilter): xcb.xcb_disconnect(conn) def nativeEventFilter( - self, evtype: Union[bytes, QByteArray], message: sip.voidptr + self, evtype: Union[bytes, QByteArray], message: Optional[sip.voidptr] ) -> Tuple[bool, _PointerRetType]: """Handle XCB events.""" # We're only installed when the platform plugin is xcb assert evtype == b"xcb_generic_event_t", evtype + assert message is not None # We cast to xcb_ge_generic_event_t, which overlaps with xcb_generic_event_t. # .extension and .event_type will only make sense if this is an diff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py index d502efa65..3396dbe51 100644 --- a/qutebrowser/misc/quitter.py +++ b/qutebrowser/misc/quitter.py @@ -37,7 +37,7 @@ except ImportError: import qutebrowser from qutebrowser.api import cmdutils -from qutebrowser.utils import log +from qutebrowser.utils import log, qtlog from qutebrowser.misc import sessions, ipc, objects from qutebrowser.mainwindow import prompt from qutebrowser.completion.models import miscmodels @@ -219,7 +219,8 @@ class Quitter(QObject): status, session)) sessions.shutdown(session, last_window=last_window) - prompt.prompt_queue.shutdown() + if prompt.prompt_queue is not None: + prompt.prompt_queue.shutdown() # If shutdown was called while we were asking a question, we're in # a still sub-eventloop (which gets quit now) and not in the main @@ -304,5 +305,5 @@ def init(args: argparse.Namespace) -> None: """Initialize the global Quitter instance.""" global instance instance = Quitter(args=args, parent=objects.qapp) - instance.shutting_down.connect(log.shutdown_log) + instance.shutting_down.connect(qtlog.shutdown_log) objects.qapp.lastWindowClosed.connect(instance.on_last_window_closed) diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index 0aac9005c..abdfd0eba 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -201,10 +201,10 @@ def simple_split(s, keep=False, maxsplit=None): if keep: pattern = '([' + whitespace + '])' - parts = re.split(pattern, s, maxsplit) + parts = re.split(pattern, s, maxsplit=maxsplit) return _combine_ws(parts, whitespace) else: pattern = '[' + whitespace + ']' - parts = re.split(pattern, s, maxsplit) + parts = re.split(pattern, s, maxsplit=maxsplit) parts[-1] = parts[-1].rstrip() return [p for p in parts if p] diff --git a/qutebrowser/qt/_core_pyqtproperty.py b/qutebrowser/qt/_core_pyqtproperty.py index ec57d4d1c..ae6435039 100644 --- a/qutebrowser/qt/_core_pyqtproperty.py +++ b/qutebrowser/qt/_core_pyqtproperty.py @@ -1,16 +1,18 @@ """WORKAROUND for missing pyqtProperty typing, ported from PyQt5-stubs: FIXME:mypy PyQt6-stubs issue -https://github.com/python-qt-tools/PyQt5-stubs/blob/5.15.6.0/PyQt5-stubs/QtCore.pyi#L70-L111 +https://github.com/python-qt-tools/PyQt5-stubs/blob/5.15.6.0/PyQt5-stubs/QtCore.pyi#L68-L111 """ # flake8: noqa -# pylint: disable=invalid-name,missing-class-docstring,too-many-arguments,redefined-builtin,unused-argument,import-error +# pylint: disable=invalid-name,missing-class-docstring,too-many-arguments,redefined-builtin,unused-argument,no-name-in-module import typing -from PyQt6.QtCore import QObjectT, pyqtSignal +from PyQt6.QtCore import QObject, pyqtSignal if typing.TYPE_CHECKING: + QObjectT = typing.TypeVar("QObjectT", bound=QObject) + TPropertyTypeVal = typing.TypeVar("TPropertyTypeVal") TPropGetter = typing.TypeVar( diff --git a/qutebrowser/qt/machinery.py b/qutebrowser/qt/machinery.py index 4e88a0634..616c7ccfc 100644 --- a/qutebrowser/qt/machinery.py +++ b/qutebrowser/qt/machinery.py @@ -3,6 +3,16 @@ """Qt wrapper selection. Contains selection logic and globals for Qt wrapper selection. + +All other files in this package are intended to be simple wrappers around Qt imports. +Depending on what is set in this module, they import from PyQt5 or PyQt6. + +The import wrappers are intended to be as thin as possible. They will not unify +API-level differences between Qt 5 and Qt 6. This is best handled by the calling code, +which has a better picture of what changed between APIs and how to best handle it. + +What they *will* do is handle simple 1:1 renames of classes, or moves between +modules (where they aim to always expose the Qt 6 API). See e.g. webenginecore.py. """ # NOTE: No qutebrowser or PyQt import should be done here (at import time), @@ -18,11 +28,13 @@ import importlib import dataclasses from typing import Optional, Dict -# Packagers: Patch the line below to change the default wrapper for Qt 6 packages, e.g.: -# sed -i 's/_DEFAULT_WRAPPER = "PyQt5"/_DEFAULT_WRAPPER = "PyQt6"/' qutebrowser/qt/machinery.py +from qutebrowser.utils import log + +# Packagers: Patch the line below to enforce a Qt wrapper, e.g.: +# sed -i 's/_WRAPPER_OVERRIDE = .*/_WRAPPER_OVERRIDE = "PyQt6"/' qutebrowser/qt/machinery.py # # Users: Set the QUTE_QT_WRAPPER environment variable to change the default wrapper. -_DEFAULT_WRAPPER = "PyQt5" +_WRAPPER_OVERRIDE = None WRAPPERS = [ "PyQt6", @@ -78,6 +90,9 @@ class SelectionReason(enum.Enum): #: The wrapper was faked/patched out (e.g. in tests). fake = "fake" + #: The wrapper was overridden by patching _WRAPPER_OVERRIDE. + override = "override" + #: The reason was not set. unknown = "unknown" @@ -150,7 +165,7 @@ def _select_wrapper(args: Optional[argparse.Namespace]) -> SelectionInfo: - If --qt-wrapper is given, use that. - Otherwise, if the QUTE_QT_WRAPPER environment variable is set, use that. - - Otherwise, use PyQt5 (FIXME:qt6 autoselect). + - Otherwise, try the wrappers in WRAPPER in order (PyQt6 -> PyQt5) """ # If any Qt wrapper has been imported before this, something strange might # be happening. @@ -168,15 +183,17 @@ def _select_wrapper(args: Optional[argparse.Namespace]) -> SelectionInfo: if env_wrapper == "auto": return _autoselect_wrapper() elif env_wrapper not in WRAPPERS: - raise Error(f"Unknown wrapper {env_wrapper} set via {env_var}, " - f"allowed: {', '.join(WRAPPERS)}") + raise Error( + f"Unknown wrapper {env_wrapper} set via {env_var}, " + f"allowed: {', '.join(WRAPPERS)}" + ) return SelectionInfo(wrapper=env_wrapper, reason=SelectionReason.env) - # FIXME:qt6 Go back to the auto-detection once ready - # FIXME:qt6 Make sure to still consider _DEFAULT_WRAPPER for packagers - # (rename to _WRAPPER_OVERRIDE since our sed command is broken anyways then?) - # return _autoselect_wrapper() - return SelectionInfo(wrapper=_DEFAULT_WRAPPER, reason=SelectionReason.default) + if _WRAPPER_OVERRIDE is not None: + assert _WRAPPER_OVERRIDE in WRAPPERS # type: ignore[unreachable] + return SelectionInfo(wrapper=_WRAPPER_OVERRIDE, reason=SelectionReason.override) + + return _autoselect_wrapper() # Values are set in init(). If you see a NameError here, it means something tried to @@ -217,8 +234,7 @@ def _set_globals(info: SelectionInfo) -> None: Those are split into multiple global variables because that way we can teach mypy about them via --always-true and --always-false, see tox.ini. """ - global INFO, USE_PYQT5, USE_PYQT6, USE_PYSIDE6, IS_QT5, IS_QT6, \ - IS_PYQT, IS_PYSIDE, _initialized + global INFO, USE_PYQT5, USE_PYQT6, USE_PYSIDE6, IS_QT5, IS_QT6, IS_PYQT, IS_PYSIDE, _initialized assert info.wrapper is not None, info assert not _initialized @@ -280,6 +296,7 @@ def init(args: argparse.Namespace) -> SelectionInfo: info = _select_wrapper(args) if info.wrapper is not None: _set_globals(info) + log.init.debug(str(info)) # If info is None here (no Qt wrapper available), we'll show an error later # in earlyinit.py. diff --git a/qutebrowser/qt/opengl.py b/qutebrowser/qt/opengl.py index 0a14dffad..bc5a31c11 100644 --- a/qutebrowser/qt/opengl.py +++ b/qutebrowser/qt/opengl.py @@ -1,4 +1,4 @@ -# pylint: disable=import-error,wildcard-import,unused-import +# pylint: disable=import-error,wildcard-import,unused-import,unused-wildcard-import """Wrapped Qt imports for Qt OpenGL. diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index e778cc23a..fcca87feb 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -50,7 +50,7 @@ except ImportError: sys.exit(100) check_python_version() -import argparse # FIXME:qt6 (lint): disable=wrong-import-order +import argparse from qutebrowser.misc import earlyinit from qutebrowser.qt import machinery diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index a8b436d79..82de30702 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -68,6 +68,7 @@ def log_signals(obj: QObject) -> QObject: def connect_log_slot(obj: QObject) -> None: """Helper function to connect all signals to a logging slot.""" metaobj = obj.metaObject() + assert metaobj is not None for i in range(metaobj.methodCount()): meta_method = metaobj.method(i) qtutils.ensure_valid(meta_method) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 521f52b5b..f2a6c396d 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -24,8 +24,6 @@ import logging import contextlib import collections import copy -import faulthandler -import traceback import warnings import json import inspect @@ -33,7 +31,9 @@ import argparse from typing import (TYPE_CHECKING, Any, Iterator, Mapping, MutableSequence, Optional, Set, Tuple, Union, TextIO, Literal, cast) -from qutebrowser.qt import core as qtcore +# NOTE: This is a Qt-free zone! All imports related to Qt logging should be done in +# qutebrowser.utils.qtlog (see https://github.com/qutebrowser/qutebrowser/issues/7769). + # Optional imports try: import colorama @@ -208,15 +208,9 @@ def init_log(args: argparse.Namespace) -> None: root.setLevel(logging.NOTSET) logging.captureWarnings(True) _init_py_warnings() - qtcore.qInstallMessageHandler(qt_message_handler) _log_inited = True -@qtcore.pyqtSlot() -def shutdown_log() -> None: - qtcore.qInstallMessageHandler(None) - - def _init_py_warnings() -> None: """Initialize Python warning handling.""" assert _args is not None @@ -231,16 +225,6 @@ def _init_py_warnings() -> None: @contextlib.contextmanager -def disable_qt_msghandler() -> Iterator[None]: - """Contextmanager which temporarily disables the Qt message handler.""" - old_handler = qtcore.qInstallMessageHandler(None) - try: - yield - finally: - qtcore.qInstallMessageHandler(old_handler) - - -@contextlib.contextmanager def py_warning_filter( action: Literal['default', 'error', 'ignore', 'always', 'module', 'once'] = 'ignore', @@ -377,163 +361,6 @@ def change_console_formatter(level: int) -> None: assert isinstance(old_formatter, JSONFormatter), old_formatter -def qt_message_handler(msg_type: qtcore.QtMsgType, - context: qtcore.QMessageLogContext, - msg: str) -> None: - """Qt message handler to redirect qWarning etc. to the logging system. - - Args: - msg_type: The level of the message. - context: The source code location of the message. - msg: The message text. - """ - # Mapping from Qt logging levels to the matching logging module levels. - # Note we map critical to ERROR as it's actually "just" an error, and fatal - # to critical. - qt_to_logging = { - qtcore.QtMsgType.QtDebugMsg: logging.DEBUG, - qtcore.QtMsgType.QtWarningMsg: logging.WARNING, - qtcore.QtMsgType.QtCriticalMsg: logging.ERROR, - qtcore.QtMsgType.QtFatalMsg: logging.CRITICAL, - qtcore.QtMsgType.QtInfoMsg: logging.INFO, - } - - # Change levels of some well-known messages to debug so they don't get - # shown to the user. - # - # If a message starts with any text in suppressed_msgs, it's not logged as - # error. - suppressed_msgs = [ - # PNGs in Qt with broken color profile - # https://bugreports.qt.io/browse/QTBUG-39788 - ('libpng warning: iCCP: Not recognizing known sRGB profile that has ' - 'been edited'), - 'libpng warning: iCCP: known incorrect sRGB profile', - # Hopefully harmless warning - 'OpenType support missing for script ', - # Error if a QNetworkReply gets two different errors set. Harmless Qt - # bug on some pages. - # https://bugreports.qt.io/browse/QTBUG-30298 - ('QNetworkReplyImplPrivate::error: Internal problem, this method must ' - 'only be called once.'), - # Sometimes indicates missing text, but most of the time harmless - 'load glyph failed ', - # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479 - ('content-type missing in HTTP POST, defaulting to ' - 'application/x-www-form-urlencoded. ' - 'Use QNetworkRequest::setHeader() to fix this problem.'), - # https://bugreports.qt.io/browse/QTBUG-43118 - 'Using blocking call!', - # Hopefully harmless - ('"Method "GetAll" with signature "s" on interface ' - '"org.freedesktop.DBus.Properties" doesn\'t exist'), - ('"Method \\"GetAll\\" with signature \\"s\\" on interface ' - '\\"org.freedesktop.DBus.Properties\\" doesn\'t exist\\n"'), - 'WOFF support requires QtWebKit to be built with zlib support.', - # Weird Enlightment/GTK X extensions - 'QXcbWindow: Unhandled client message: "_E_', - 'QXcbWindow: Unhandled client message: "_ECORE_', - 'QXcbWindow: Unhandled client message: "_GTK_', - # Happens on AppVeyor CI - 'SetProcessDpiAwareness failed:', - # https://bugreports.qt.io/browse/QTBUG-49174 - ('QObject::connect: Cannot connect (null)::stateChanged(' - 'QNetworkSession::State) to ' - 'QNetworkReplyHttpImpl::_q_networkSessionStateChanged(' - 'QNetworkSession::State)'), - # https://bugreports.qt.io/browse/QTBUG-53989 - ("Image of format '' blocked because it is not considered safe. If " - "you are sure it is safe to do so, you can white-list the format by " - "setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST="), - # Installing Qt from the installer may cause it looking for SSL3 or - # OpenSSL 1.0 which may not be available on the system - "QSslSocket: cannot resolve ", - "QSslSocket: cannot call unresolved function ", - # When enabling debugging with QtWebEngine - ("Remote debugging server started successfully. Try pointing a " - "Chromium-based browser to "), - # https://github.com/qutebrowser/qutebrowser/issues/1287 - "QXcbClipboard: SelectionRequest too old", - # https://github.com/qutebrowser/qutebrowser/issues/2071 - 'QXcbWindow: Unhandled client message: ""', - # https://codereview.qt-project.org/176831 - "QObject::disconnect: Unexpected null parameter", - # https://bugreports.qt.io/browse/QTBUG-76391 - "Attribute Qt::AA_ShareOpenGLContexts must be set before " - "QCoreApplication is created.", - # Qt 6.4 beta 1: https://bugreports.qt.io/browse/QTBUG-104741 - "GL format 0 is not supported", - ] - # not using utils.is_mac here, because we can't be sure we can successfully - # import the utils module here. - if sys.platform == 'darwin': - suppressed_msgs += [ - # https://bugreports.qt.io/browse/QTBUG-47154 - ('virtual void QSslSocketBackendPrivate::transmit() SSLRead ' - 'failed with: -9805'), - ] - - if not msg: - msg = "Logged empty message!" - - if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): - level = logging.DEBUG - elif context.category == "qt.webenginecontext" and ( - msg.strip().startswith("GL Type: ") or # Qt 6.3 - msg.strip().startswith("GLImplementation:") # Qt 6.2 - ): - level = logging.DEBUG - else: - level = qt_to_logging[msg_type] - - if context.line is None: - lineno = -1 # type: ignore[unreachable] - else: - lineno = context.line - - if context.function is None: - func = 'none' # type: ignore[unreachable] - elif ':' in context.function: - func = '"{}"'.format(context.function) - else: - func = context.function - - if context.category is None or context.category == 'default': - name = 'qt' - else: - name = 'qt-' + context.category - if msg.splitlines()[0] == ('This application failed to start because it ' - 'could not find or load the Qt platform plugin ' - '"xcb".'): - # Handle this message specially. - msg += ("\n\nOn Archlinux, this should fix the problem:\n" - " pacman -S libxkbcommon-x11") - faulthandler.disable() - - assert _args is not None - if _args.debug: - stack: Optional[str] = ''.join(traceback.format_stack()) - else: - stack = None - - record = qt.makeRecord(name=name, level=level, fn=context.file, lno=lineno, - msg=msg, args=(), exc_info=None, func=func, - sinfo=stack) - qt.handle(record) - - -@contextlib.contextmanager -def hide_qt_warning(pattern: str, logger: str = 'qt') -> Iterator[None]: - """Hide Qt warnings matching the given regex.""" - log_filter = QtWarningFilter(pattern) - logger_obj = logging.getLogger(logger) - logger_obj.addFilter(log_filter) - try: - yield - finally: - logger_obj.removeFilter(log_filter) - - def init_from_config(conf: 'configmodule.ConfigContainer') -> None: """Initialize logging settings from the config. @@ -564,24 +391,6 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -> None: change_console_formatter(level) -class QtWarningFilter(logging.Filter): - - """Filter to filter Qt warnings. - - Attributes: - _pattern: The start of the message. - """ - - def __init__(self, pattern: str) -> None: - super().__init__() - self._pattern = pattern - - def filter(self, record: logging.LogRecord) -> bool: - """Determine if the specified record is to be logged.""" - do_log = not record.msg.strip().startswith(self._pattern) - return do_log - - class InvalidLogFilterError(Exception): """Raised when an invalid filter string is passed to LogFilter.parse().""" diff --git a/qutebrowser/utils/qtlog.py b/qutebrowser/utils/qtlog.py new file mode 100644 index 000000000..15e124b79 --- /dev/null +++ b/qutebrowser/utils/qtlog.py @@ -0,0 +1,241 @@ +# Copyright 2014-2023 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. + +"""Loggers and utilities related to Qt logging.""" + +import argparse +import contextlib +import faulthandler +import logging +import sys +import traceback +from typing import Iterator, Optional, Callable, cast + +from qutebrowser.qt import core as qtcore, machinery +from qutebrowser.utils import log + +_args = None + + +def init(args: argparse.Namespace) -> None: + """Install Qt message handler based on the argparse namespace passed.""" + global _args + _args = args + qtcore.qInstallMessageHandler(qt_message_handler) + + +@qtcore.pyqtSlot() +def shutdown_log() -> None: + qtcore.qInstallMessageHandler(None) + + +@contextlib.contextmanager +def disable_qt_msghandler() -> Iterator[None]: + """Contextmanager which temporarily disables the Qt message handler.""" + old_handler = qtcore.qInstallMessageHandler(None) + if machinery.IS_QT6: + # cast str to Optional[str] to be compatible with PyQt6 type hints for + # qInstallMessageHandler + old_handler = cast( + Optional[ + Callable[ + [qtcore.QtMsgType, qtcore.QMessageLogContext, Optional[str]], + None + ] + ], + old_handler, + ) + + try: + yield + finally: + qtcore.qInstallMessageHandler(old_handler) + + +def qt_message_handler(msg_type: qtcore.QtMsgType, + context: qtcore.QMessageLogContext, + msg: Optional[str]) -> None: + """Qt message handler to redirect qWarning etc. to the logging system. + + Args: + msg_type: The level of the message. + context: The source code location of the message. + msg: The message text. + """ + # Mapping from Qt logging levels to the matching logging module levels. + # Note we map critical to ERROR as it's actually "just" an error, and fatal + # to critical. + qt_to_logging = { + qtcore.QtMsgType.QtDebugMsg: logging.DEBUG, + qtcore.QtMsgType.QtWarningMsg: logging.WARNING, + qtcore.QtMsgType.QtCriticalMsg: logging.ERROR, + qtcore.QtMsgType.QtFatalMsg: logging.CRITICAL, + qtcore.QtMsgType.QtInfoMsg: logging.INFO, + } + + # Change levels of some well-known messages to debug so they don't get + # shown to the user. + # + # If a message starts with any text in suppressed_msgs, it's not logged as + # error. + suppressed_msgs = [ + # PNGs in Qt with broken color profile + # https://bugreports.qt.io/browse/QTBUG-39788 + ('libpng warning: iCCP: Not recognizing known sRGB profile that has ' + 'been edited'), + 'libpng warning: iCCP: known incorrect sRGB profile', + # Hopefully harmless warning + 'OpenType support missing for script ', + # Error if a QNetworkReply gets two different errors set. Harmless Qt + # bug on some pages. + # https://bugreports.qt.io/browse/QTBUG-30298 + ('QNetworkReplyImplPrivate::error: Internal problem, this method must ' + 'only be called once.'), + # Sometimes indicates missing text, but most of the time harmless + 'load glyph failed ', + # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479 + ('content-type missing in HTTP POST, defaulting to ' + 'application/x-www-form-urlencoded. ' + 'Use QNetworkRequest::setHeader() to fix this problem.'), + # https://bugreports.qt.io/browse/QTBUG-43118 + 'Using blocking call!', + # Hopefully harmless + ('"Method "GetAll" with signature "s" on interface ' + '"org.freedesktop.DBus.Properties" doesn\'t exist'), + ('"Method \\"GetAll\\" with signature \\"s\\" on interface ' + '\\"org.freedesktop.DBus.Properties\\" doesn\'t exist\\n"'), + 'WOFF support requires QtWebKit to be built with zlib support.', + # Weird Enlightment/GTK X extensions + 'QXcbWindow: Unhandled client message: "_E_', + 'QXcbWindow: Unhandled client message: "_ECORE_', + 'QXcbWindow: Unhandled client message: "_GTK_', + # Happens on AppVeyor CI + 'SetProcessDpiAwareness failed:', + # https://bugreports.qt.io/browse/QTBUG-49174 + ('QObject::connect: Cannot connect (null)::stateChanged(' + 'QNetworkSession::State) to ' + 'QNetworkReplyHttpImpl::_q_networkSessionStateChanged(' + 'QNetworkSession::State)'), + # https://bugreports.qt.io/browse/QTBUG-53989 + ("Image of format '' blocked because it is not considered safe. If " + "you are sure it is safe to do so, you can white-list the format by " + "setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST="), + # Installing Qt from the installer may cause it looking for SSL3 or + # OpenSSL 1.0 which may not be available on the system + "QSslSocket: cannot resolve ", + "QSslSocket: cannot call unresolved function ", + # When enabling debugging with QtWebEngine + ("Remote debugging server started successfully. Try pointing a " + "Chromium-based browser to "), + # https://github.com/qutebrowser/qutebrowser/issues/1287 + "QXcbClipboard: SelectionRequest too old", + # https://github.com/qutebrowser/qutebrowser/issues/2071 + 'QXcbWindow: Unhandled client message: ""', + # https://codereview.qt-project.org/176831 + "QObject::disconnect: Unexpected null parameter", + # https://bugreports.qt.io/browse/QTBUG-76391 + "Attribute Qt::AA_ShareOpenGLContexts must be set before " + "QCoreApplication is created.", + # Qt 6.4 beta 1: https://bugreports.qt.io/browse/QTBUG-104741 + "GL format 0 is not supported", + ] + # not using utils.is_mac here, because we can't be sure we can successfully + # import the utils module here. + if sys.platform == 'darwin': + suppressed_msgs += [ + # https://bugreports.qt.io/browse/QTBUG-47154 + ('virtual void QSslSocketBackendPrivate::transmit() SSLRead ' + 'failed with: -9805'), + ] + + if not msg: + msg = "Logged empty message!" + + if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): + level = logging.DEBUG + elif context.category == "qt.webenginecontext" and ( + msg.strip().startswith("GL Type: ") or # Qt 6.3 + msg.strip().startswith("GLImplementation:") # Qt 6.2 + ): + level = logging.DEBUG + else: + level = qt_to_logging[msg_type] + + if context.line is None: + lineno = -1 # type: ignore[unreachable] + else: + lineno = context.line + + if context.function is None: + func = 'none' # type: ignore[unreachable] + elif ':' in context.function: + func = '"{}"'.format(context.function) + else: + func = context.function + + if context.category is None or context.category == 'default': + name = 'qt' + else: + name = 'qt-' + context.category + if msg.splitlines()[0] == ('This application failed to start because it ' + 'could not find or load the Qt platform plugin ' + '"xcb".'): + # Handle this message specially. + msg += ("\n\nOn Archlinux, this should fix the problem:\n" + " pacman -S libxkbcommon-x11") + faulthandler.disable() + + assert _args is not None + if _args.debug: + stack: Optional[str] = ''.join(traceback.format_stack()) + else: + stack = None + + record = log.qt.makeRecord(name=name, level=level, fn=context.file, lno=lineno, + msg=msg, args=(), exc_info=None, func=func, + sinfo=stack) + log.qt.handle(record) + + +class QtWarningFilter(logging.Filter): + + """Filter to filter Qt warnings. + + Attributes: + _pattern: The start of the message. + """ + + def __init__(self, pattern: str) -> None: + super().__init__() + self._pattern = pattern + + def filter(self, record: logging.LogRecord) -> bool: + """Determine if the specified record is to be logged.""" + do_log = not record.msg.strip().startswith(self._pattern) + return do_log + + +@contextlib.contextmanager +def hide_qt_warning(pattern: str, logger: str = 'qt') -> Iterator[None]: + """Hide Qt warnings matching the given regex.""" + log_filter = QtWarningFilter(pattern) + logger_obj = logging.getLogger(logger) + logger_obj.addFilter(log_filter) + try: + yield + finally: + logger_obj.removeFilter(log_filter) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index cc34057ef..beebcc5c2 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -32,7 +32,7 @@ import pathlib import operator import contextlib from typing import (Any, AnyStr, TYPE_CHECKING, BinaryIO, IO, Iterator, - Optional, Union, Tuple, Protocol, cast) + Optional, Union, Tuple, Protocol, cast, TypeVar) from qutebrowser.qt import machinery, sip from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray, @@ -46,6 +46,7 @@ except ImportError: # pragma: no cover if TYPE_CHECKING: from qutebrowser.qt.webkit import QWebHistory from qutebrowser.qt.webenginecore import QWebEngineHistory + from typing_extensions import TypeGuard # added in Python 3.10 from qutebrowser.misc import objects from qutebrowser.utils import usertypes, utils @@ -102,7 +103,11 @@ def version_check(version: str, parsed = utils.VersionNumber.parse(version) op = operator.eq if exact else operator.ge - result = op(utils.VersionNumber.parse(qVersion()), parsed) + + qversion = qVersion() + assert qversion is not None + result = op(utils.VersionNumber.parse(qversion), parsed) + if compiled and result: # qVersion() ==/>= parsed, now check if QT_VERSION_STR ==/>= parsed. result = op(utils.VersionNumber.parse(QT_VERSION_STR), parsed) @@ -535,24 +540,58 @@ def interpolate_color( if colorspace is None: if percent == 100: - return QColor(*end.getRgb()) + r, g, b, a = end.getRgb() + assert r is not None + assert g is not None + assert b is not None + assert a is not None + return QColor(r, g, b, a) else: - return QColor(*start.getRgb()) + r, g, b, a = start.getRgb() + assert r is not None + assert g is not None + assert b is not None + assert a is not None + return QColor(r, g, b, a) out = QColor() if colorspace == QColor.Spec.Rgb: r1, g1, b1, a1 = start.getRgb() r2, g2, b2, a2 = end.getRgb() + assert r1 is not None + assert g1 is not None + assert b1 is not None + assert a1 is not None + assert r2 is not None + assert g2 is not None + assert b2 is not None + assert a2 is not None components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent) out.setRgb(*components) elif colorspace == QColor.Spec.Hsv: h1, s1, v1, a1 = start.getHsv() h2, s2, v2, a2 = end.getHsv() + assert h1 is not None + assert s1 is not None + assert v1 is not None + assert a1 is not None + assert h2 is not None + assert s2 is not None + assert v2 is not None + assert a2 is not None components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent) out.setHsv(*components) elif colorspace == QColor.Spec.Hsl: h1, s1, l1, a1 = start.getHsl() h2, s2, l2, a2 = end.getHsl() + assert h1 is not None + assert s1 is not None + assert l1 is not None + assert a1 is not None + assert h2 is not None + assert s2 is not None + assert l2 is not None + assert a2 is not None components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent) out.setHsl(*components) else: @@ -611,3 +650,31 @@ def extract_enum_val(val: Union[sip.simplewrapper, int, enum.Enum]) -> int: elif isinstance(val, sip.simplewrapper): return int(val) # type: ignore[call-overload] return val + + +_T = TypeVar("_T") + + +if machinery.IS_QT5: + # On Qt 5, add/remove Optional where type annotations don't have it. + # Also we have a special QT_NONE, which (being Any) we can pass to functions + # where PyQt type hints claim that it's not allowed. + + def remove_optional(obj: Optional[_T]) -> _T: + return cast(_T, obj) + + def add_optional(obj: _T) -> Optional[_T]: + return cast(Optional[_T], obj) + + QT_NONE: Any = None +else: + # On Qt 6, all those things are handled correctly by type annotations, so we + # have a no-op below. + + def remove_optional(obj: Optional[_T]) -> Optional[_T]: + return obj + + def add_optional(obj: Optional[_T]) -> Optional[_T]: + return obj + + QT_NONE = None diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 884a26376..a1fa414f7 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -28,7 +28,7 @@ from typing import Iterator, Optional from qutebrowser.qt.core import QStandardPaths from qutebrowser.qt.widgets import QApplication -from qutebrowser.utils import log, debug, utils, version +from qutebrowser.utils import log, debug, utils, version, qtutils # The cached locations _locations = {} @@ -65,7 +65,7 @@ def _unset_organization() -> Iterator[None]: qapp = QApplication.instance() if qapp is not None: orgname = qapp.organizationName() - qapp.setOrganizationName(None) # type: ignore[arg-type] + qapp.setOrganizationName(qtutils.QT_NONE) try: yield finally: diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index e00c9dab2..1bb035939 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -179,9 +179,9 @@ def _get_search_url(txt: str) -> QUrl: url = QUrl.fromUserInput(evaluated) else: url = QUrl.fromUserInput(config.val.url.searchengines[engine]) - url.setPath(None) # type: ignore[arg-type] - url.setFragment(None) # type: ignore[arg-type] - url.setQuery(None) # type: ignore[call-overload] + url.setPath(qtutils.QT_NONE) + url.setFragment(qtutils.QT_NONE) + url.setQuery(qtutils.QT_NONE) qtutils.ensure_valid(url) return url diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index a81952b7d..dd3cf6ac3 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -128,17 +128,20 @@ class VersionNumber: return NotImplemented return self._ver != other._ver + # FIXME:mypy type ignores below needed for PyQt5-stubs: + # Unsupported left operand type for ... ("QVersionNumber") + def __ge__(self, other: 'VersionNumber') -> bool: - return self._ver >= other._ver # type: ignore[operator] + return self._ver >= other._ver # type: ignore[operator,unused-ignore] def __gt__(self, other: 'VersionNumber') -> bool: - return self._ver > other._ver # type: ignore[operator] + return self._ver > other._ver # type: ignore[operator,unused-ignore] def __le__(self, other: 'VersionNumber') -> bool: - return self._ver <= other._ver # type: ignore[operator] + return self._ver <= other._ver # type: ignore[operator,unused-ignore] def __lt__(self, other: 'VersionNumber') -> bool: - return self._ver < other._ver # type: ignore[operator] + return self._ver < other._ver # type: ignore[operator,unused-ignore] class Unreachable(Exception): @@ -516,6 +519,13 @@ def sanitize_filename(name: str, return name +def _clipboard() -> QClipboard: + """Get the QClipboard and make sure it's not None.""" + clipboard = QApplication.clipboard() + assert clipboard is not None + return clipboard + + def set_clipboard(data: str, selection: bool = False) -> None: """Set the clipboard to some given data.""" global fake_clipboard @@ -527,7 +537,7 @@ def set_clipboard(data: str, selection: bool = False) -> None: fake_clipboard = data else: mode = QClipboard.Mode.Selection if selection else QClipboard.Mode.Clipboard - QApplication.clipboard().setText(data, mode=mode) + _clipboard().setText(data, mode=mode) def get_clipboard(selection: bool = False, fallback: bool = False) -> str: @@ -553,7 +563,7 @@ def get_clipboard(selection: bool = False, fallback: bool = False) -> str: fake_clipboard = None else: mode = QClipboard.Mode.Selection if selection else QClipboard.Mode.Clipboard - data = QApplication.clipboard().text(mode=mode) + data = _clipboard().text(mode=mode) target = "Primary selection" if selection else "Clipboard" if not data.strip(): @@ -565,7 +575,7 @@ def get_clipboard(selection: bool = False, fallback: bool = False) -> str: def supports_selection() -> bool: """Check if the OS supports primary selection.""" - return QApplication.clipboard().supportsSelection() + return _clipboard().supportsSelection() def open_file(filename: str, cmdline: str = None) -> None: diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 782261745..43d6e4d06 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -32,7 +32,7 @@ import getpass import functools import dataclasses import importlib.metadata -from typing import (Mapping, Optional, Sequence, Tuple, ClassVar, Dict, cast, Any, +from typing import (Mapping, Optional, Sequence, Tuple, ClassVar, Dict, Any, TYPE_CHECKING) from qutebrowser.qt import machinery @@ -886,7 +886,10 @@ def version_info() -> str: if objects.qapp: style = objects.qapp.style() - lines.append('Style: {}'.format(style.metaObject().className())) + assert style is not None + metaobj = style.metaObject() + assert metaobj is not None + lines.append('Style: {}'.format(metaobj.className())) lines.append('Platform plugin: {}'.format(objects.qapp.platformName())) lines.append('OpenGL: {}'.format(opengl_info())) @@ -1005,7 +1008,7 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover vendor, version = override.split(', ', maxsplit=1) return OpenGLInfo.parse(vendor=vendor, version=version) - old_context = cast(Optional[QOpenGLContext], QOpenGLContext.currentContext()) + old_context: Optional[QOpenGLContext] = QOpenGLContext.currentContext() old_surface = None if old_context is None else old_context.surface() surface = QOffscreenSurface() diff --git a/requirements.txt b/requirements.txt index e6316e88b..b5bab3296 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,12 +2,12 @@ adblock==0.6.0 colorama==0.4.6 -importlib-resources==5.12.0 ; python_version=="3.8.*" +importlib-resources==6.0.0 ; python_version=="3.8.*" Jinja2==3.1.2 MarkupSafe==2.1.3 Pygments==2.15.1 -PyYAML==6.0 -zipp==3.15.0 +PyYAML==6.0.1 +zipp==3.16.2 # Unpinned due to recompile_requirements.py limitations pyobjc-core ; sys_platform=="darwin" pyobjc-framework-Cocoa ; sys_platform=="darwin" diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 996487693..d8f9693e7 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -136,7 +136,7 @@ def _smoke_test_run( return subprocess.run(argv, check=True, capture_output=True) -def smoke_test(executable: pathlib.Path, debug: bool, qt6: bool) -> None: +def smoke_test(executable: pathlib.Path, debug: bool, qt5: bool) -> None: """Try starting the given qutebrowser executable.""" stdout_whitelist = [] stderr_whitelist = [ @@ -176,7 +176,7 @@ def smoke_test(executable: pathlib.Path, debug: bool, qt6: bool) -> None: r'ContextResult::kTransientFailure: Failed to send ' r'.*CreateCommandBuffer\.'), ]) - if qt6: + if not qt5: stderr_whitelist.extend([ # FIXME:qt6 Qt 6.3 on macOS r'[0-9:]* WARNING: Incompatible version of OpenSSL', @@ -257,10 +257,10 @@ def verify_windows_exe(exe_path: pathlib.Path) -> None: assert pe.verify_checksum() -def patch_mac_app(qt6: bool) -> None: +def patch_mac_app(qt5: bool) -> None: """Patch .app to save some space and make it signable.""" dist_path = pathlib.Path('dist') - ver = '6' if qt6 else '5' + ver = '5' if qt5 else '6' app_path = dist_path / 'qutebrowser.app' contents_path = app_path / 'Contents' @@ -280,7 +280,7 @@ def patch_mac_app(qt6: bool) -> None: file_path.unlink() file_path.symlink_to(target) - if qt6: + if not qt5: # Symlinking QtWebEngineCore.framework does not seem to work with Qt 6. # Also, the symlinking/moving before signing doesn't seem to be required. return @@ -333,7 +333,7 @@ def _mac_bin_path(base: pathlib.Path) -> pathlib.Path: def build_mac( *, gh_token: Optional[str], - qt6: bool, + qt5: bool, skip_packaging: bool, debug: bool, ) -> List[Artifact]: @@ -348,20 +348,20 @@ def build_mac( shutil.rmtree(d, ignore_errors=True) utils.print_title("Updating 3rdparty content") - update_3rdparty.run(ace=False, pdfjs=True, legacy_pdfjs=not qt6, fancy_dmg=False, + update_3rdparty.run(ace=False, pdfjs=True, legacy_pdfjs=qt5, fancy_dmg=False, gh_token=gh_token) utils.print_title("Building .app via pyinstaller") - call_tox(f'pyinstaller-64bit{"-qt6" if qt6 else ""}', '-r', debug=debug) + call_tox(f'pyinstaller-64bit{"-qt5" if qt5 else ""}', '-r', debug=debug) utils.print_title("Patching .app") - patch_mac_app(qt6=qt6) + patch_mac_app(qt5=qt5) utils.print_title("Re-signing .app") sign_mac_app() dist_path = pathlib.Path("dist") utils.print_title("Running pre-dmg smoke test") - smoke_test(_mac_bin_path(dist_path), debug=debug, qt6=qt6) + smoke_test(_mac_bin_path(dist_path), debug=debug, qt5=qt5) if skip_packaging: return [] @@ -371,7 +371,7 @@ def build_mac( subprocess.run(['make', '-f', dmg_makefile_path], check=True) suffix = "-debug" if debug else "" - suffix += "-qt6" if qt6 else "" + suffix += "-qt5" if qt5 else "" dmg_path = dist_path / f'qutebrowser-{qutebrowser.__version__}{suffix}.dmg' pathlib.Path('qutebrowser.dmg').rename(dmg_path) @@ -383,7 +383,7 @@ def build_mac( subprocess.run(['hdiutil', 'attach', dmg_path, '-mountpoint', tmp_path], check=True) try: - smoke_test(_mac_bin_path(tmp_path), debug=debug, qt6=qt6) + smoke_test(_mac_bin_path(tmp_path), debug=debug, qt5=qt5) finally: print("Waiting 10s for dmg to be detachable...") time.sleep(10) @@ -422,7 +422,7 @@ def _get_windows_python_path(x64: bool) -> pathlib.Path: def _build_windows_single( *, x64: bool, - qt6: bool, + qt5: bool, skip_packaging: bool, debug: bool, ) -> List[Artifact]: @@ -437,9 +437,9 @@ def _build_windows_single( python = _get_windows_python_path(x64=x64) suffix = "64bit" if x64 else "32bit" - if qt6: + if qt5: # FIXME:qt6 does this regress 391623d5ec983ecfc4512c7305c4b7a293ac3872? - suffix += "-qt6" + suffix += "-qt5" call_tox(f'pyinstaller-{suffix}', '-r', python=python, debug=debug) out_pyinstaller = dist_path / "qutebrowser" @@ -450,7 +450,7 @@ def _build_windows_single( verify_windows_exe(exe_path) utils.print_title(f"Running {human_arch} smoke test") - smoke_test(exe_path, debug=debug, qt6=qt6) + smoke_test(exe_path, debug=debug, qt5=qt5) if skip_packaging: return [] @@ -463,7 +463,7 @@ def _build_windows_single( desc_arch=human_arch, desc_suffix='' if x64 else ' (only for 32-bit Windows!)', debug=debug, - qt6=qt6, + qt5=qt5, ) @@ -472,12 +472,12 @@ def build_windows( skip_packaging: bool, only_32bit: bool, only_64bit: bool, - qt6: bool, + qt5: bool, debug: bool, ) -> List[Artifact]: """Build windows executables/setups.""" utils.print_title("Updating 3rdparty content") - update_3rdparty.run(nsis=True, ace=False, pdfjs=True, legacy_pdfjs=not qt6, + update_3rdparty.run(nsis=True, ace=False, pdfjs=True, legacy_pdfjs=qt5, fancy_dmg=False, gh_token=gh_token) utils.print_title("Building Windows binaries") @@ -493,14 +493,14 @@ def build_windows( x64=True, skip_packaging=skip_packaging, debug=debug, - qt6=qt6, + qt5=qt5, ) - if not only_64bit and not qt6: + if not only_64bit and not qt5: artifacts += _build_windows_single( x64=False, skip_packaging=skip_packaging, debug=debug, - qt6=qt6, + qt5=qt5, ) return artifacts @@ -514,7 +514,7 @@ def _package_windows_single( desc_suffix: str, filename_arch: str, debug: bool, - qt6: bool, + qt5: bool, ) -> List[Artifact]: """Build the given installer/zip for windows.""" artifacts = [] @@ -532,8 +532,8 @@ def _package_windows_single( ] if debug: name_parts.append('debug') - if qt6: - name_parts.append('qt6') + if qt5: + name_parts.append('qt5') name = '-'.join(name_parts) + '.exe' artifacts.append(Artifact( @@ -552,8 +552,8 @@ def _package_windows_single( ] if debug: zip_name_parts.append('debug') - if qt6: - zip_name_parts.append('qt6') + if qt5: + zip_name_parts.append('qt5') zip_name = '-'.join(zip_name_parts) + '.zip' zip_path = dist_path / zip_name @@ -738,8 +738,8 @@ def main() -> None: help="Skip Windows 32 bit build.", dest='only_64bit') parser.add_argument('--debug', action='store_true', required=False, help="Build a debug build.") - parser.add_argument('--qt6', action='store_true', required=False, - help="Build against PyQt6") + parser.add_argument('--qt5', action='store_true', required=False, + help="Build against PyQt5") args = parser.parse_args() utils.change_cwd() @@ -768,14 +768,14 @@ def main() -> None: skip_packaging=args.skip_packaging, only_32bit=args.only_32bit, only_64bit=args.only_64bit, - qt6=args.qt6, + qt5=args.qt5, debug=args.debug, ) elif IS_MACOS: artifacts = build_mac( gh_token=gh_token, skip_packaging=args.skip_packaging, - qt6=args.qt6, + qt5=args.qt5, debug=args.debug, ) else: diff --git a/scripts/dev/changelog_urls.json b/scripts/dev/changelog_urls.json index 9be31c03f..89d3b332a 100644 --- a/scripts/dev/changelog_urls.json +++ b/scripts/dev/changelog_urls.json @@ -104,7 +104,6 @@ "PyQt-builder": "https://www.riverbankcomputing.com/news", "PyQt5-sip": "https://www.riverbankcomputing.com/news", "PyQt5-stubs": "https://github.com/python-qt-tools/PyQt5-stubs/blob/master/CHANGELOG.md", - "PyQt6-stubs": "https://github.com/python-qt-tools/PyQt6-stubs/commits/main", "sip": "https://www.riverbankcomputing.com/news", "PyQt6": "https://www.riverbankcomputing.com/news", "PyQt6-Qt6": "https://www.riverbankcomputing.com/news", @@ -138,8 +137,8 @@ "pyroma": "https://github.com/regebro/pyroma/blob/master/CHANGES.txt", "adblock": "https://github.com/ArniDagur/python-adblock/blob/master/CHANGELOG.md", "importlib-resources": "https://importlib-resources.readthedocs.io/en/latest/history.html", - "importlib-metadata": "https://github.com/python/importlib_metadata/blob/main/CHANGES.rst", - "zipp": "https://github.com/jaraco/zipp/blob/main/CHANGES.rst", + "importlib-metadata": "https://github.com/python/importlib_metadata/blob/main/NEWS.rst", + "zipp": "https://github.com/jaraco/zipp/blob/main/NEWS.rst", "pip": "https://pip.pypa.io/en/stable/news/", "wheel": "https://wheel.readthedocs.io/en/stable/news.html", "setuptools": "https://setuptools.readthedocs.io/en/latest/history.html", @@ -148,7 +147,7 @@ "bleach": "https://github.com/mozilla/bleach/blob/main/CHANGES", "jeepney": "https://gitlab.com/takluyver/jeepney/-/blob/master/docs/release-notes.rst", "keyring": "https://github.com/jaraco/keyring/blob/main/NEWS.rst", - "jaraco.classes": "https://github.com/jaraco/jaraco.classes/blob/main/CHANGES.rst", + "jaraco.classes": "https://github.com/jaraco/jaraco.classes/blob/main/NEWS.rst", "pkginfo": "https://bazaar.launchpad.net/~tseaver/pkginfo/trunk/view/head:/CHANGES.txt", "readme-renderer": "https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst", "requests-toolbelt": "https://github.com/requests/toolbelt/blob/master/HISTORY.rst", diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 240b5e6f1..215a1cfa0 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -27,7 +27,7 @@ import subprocess import tokenize import traceback import pathlib -from typing import List, Iterator, Optional +from typing import List, Iterator, Optional, Tuple REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO_ROOT)) @@ -152,6 +152,24 @@ def _check_spelling_file(path, fobj, patterns): return ok +def _check_spelling_all( + args: argparse.Namespace, + ignored: List[pathlib.Path], + patterns: List[Tuple[re.Pattern, str]], +) -> Optional[bool]: + try: + ok = True + for path in _get_files(verbose=args.verbose, ignored=ignored): + with tokenize.open(str(path)) as f: + if not _check_spelling_file(path, f, patterns): + ok = False + print() + return ok + except Exception: + traceback.print_exc() + return None + + def check_spelling(args: argparse.Namespace) -> Optional[bool]: """Check commonly misspelled words.""" # Words which I often misspell @@ -273,25 +291,13 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: hint_data / 'ace' / 'ace.js', hint_data / 'bootstrap' / 'bootstrap.css', ] - - try: - ok = True - for path in _get_files(verbose=args.verbose, ignored=ignored): - with tokenize.open(path) as f: - if not _check_spelling_file(path, f, patterns): - ok = False - print() - return ok - except Exception: - traceback.print_exc() - return None + return _check_spelling_all(args=args, ignored=ignored, patterns=patterns) def check_pyqt_imports(args: argparse.Namespace) -> Optional[bool]: """Check for direct PyQt imports.""" ignored = [ pathlib.Path("qutebrowser", "qt"), - # FIXME:qt6 fix those too? pathlib.Path("misc", "userscripts"), pathlib.Path("scripts"), ] @@ -305,18 +311,7 @@ def check_pyqt_imports(args: argparse.Namespace) -> Optional[bool]: "Use 'import qutebrowser.qt.MODULE' instead", ) ] - # FIXME:qt6 unify this with check_spelling somehow? - try: - ok = True - for path in _get_files(verbose=args.verbose, ignored=ignored): - with tokenize.open(str(path)) as f: - if not _check_spelling_file(path, f, patterns): - ok = False - print() - return ok - except Exception: - traceback.print_exc() - return None + return _check_spelling_all(args=args, ignored=ignored, patterns=patterns) def check_vcs_conflict(args: argparse.Namespace) -> Optional[bool]: diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 960b5a514..1e7ed0f61 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -60,7 +60,6 @@ def whitelist_generator(): # noqa: C901 yield 'qutebrowser.misc.sql.SqliteErrorCode.CONSTRAINT' yield 'qutebrowser.misc.throttle.Throttle.set_delay' yield 'qutebrowser.misc.guiprocess.GUIProcess.stderr' - yield 'qutebrowser.qt.machinery._autoselect_wrapper' # FIXME:qt6 # Qt attributes yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl' diff --git a/scripts/dev/standardpaths_tester.py b/scripts/dev/standardpaths_tester.py index ff85b2a4c..bbd0a39fb 100644 --- a/scripts/dev/standardpaths_tester.py +++ b/scripts/dev/standardpaths_tester.py @@ -21,7 +21,7 @@ import os import sys -from PyQt5.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion, +from PyQt6.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion, QStandardPaths, QCoreApplication) diff --git a/scripts/keytester.py b/scripts/keytester.py index 6d994114d..861133c06 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -21,8 +21,7 @@ Use python3 -m scripts.keytester to launch it. """ -from PyQt5.QtWidgets import QApplication - +from qutebrowser.qt.widgets import QApplication from qutebrowser.misc import miscwidgets app = QApplication([]) diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index 4581bef41..63bdde959 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -125,7 +125,7 @@ def get_lib_path(executable, name, required=True): raise ValueError("Unexpected output: {!r}".format(output)) -def link_pyqt(executable, venv_path, *, version='5'): +def link_pyqt(executable, venv_path, *, version): """Symlink the systemwide PyQt/sip into the venv. Args: diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py index 625cedd1a..3fe911331 100755 --- a/scripts/mkvenv.py +++ b/scripts/mkvenv.py @@ -134,8 +134,7 @@ def pyqt_versions() -> List[str]: def _is_qt6_version(version: str) -> bool: """Check if the given version is Qt 6.""" - # FIXME:qt6 Adjust once auto = Qt 6 - return version == "6" or version.startswith("6.") + return version in ["auto", "6"] or version.startswith("6.") def run_venv( @@ -228,7 +227,7 @@ def requirements_file(name: str) -> pathlib.Path: def pyqt_requirements_file(version: str) -> pathlib.Path: """Get the filename of the requirements file for the given PyQt version.""" - name = 'pyqt' if version == 'auto' else 'pyqt-{}'.format(version) + name = 'pyqt-6' if version == 'auto' else f'pyqt-{version}' return requirements_file(name) @@ -277,7 +276,8 @@ def install_pyqt_link(venv_dir: pathlib.Path, version: str) -> None: """Install PyQt by linking a system-wide install.""" utils.print_title("Linking system-wide PyQt") lib_path = link_pyqt.get_venv_lib_path(str(venv_dir)) - link_pyqt.link_pyqt(sys.executable, lib_path, version=version) + major_version: str = "6" if _is_qt6_version(version) else "5" + link_pyqt.link_pyqt(sys.executable, lib_path, version=major_version) def install_pyqt_wheels(venv_dir: pathlib.Path, @@ -439,7 +439,7 @@ def run_qt_smoke_test_single( def run_qt_smoke_test(venv_dir: pathlib.Path, *, pyqt_version: str) -> None: """Make sure the Qt installation works.""" # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-104415 - no_debug = pyqt_version in ("6.3", "6") and sys.platform == "darwin" + no_debug = pyqt_version == "6.3" and sys.platform == "darwin" if no_debug: try: run_qt_smoke_test_single(venv_dir, debug=False, pyqt_version=pyqt_version) @@ -505,6 +505,9 @@ def install_pyqt(venv_dir, args): install_pyqt_binary(venv_dir, args.pyqt_version) if args.pyqt_snapshot: install_pyqt_shapshot(venv_dir, args.pyqt_snapshot.split(',')) + # Workaround until pyqt 6.5.2 is released on pypi + elif args.pyqt_version in ("6.5", "6", "auto"): + install_pyqt_shapshot(venv_dir, ["PyQt6-Qt6", "PyQt6-WebEngine-Qt6"]) elif args.pyqt_type == 'source': install_pyqt_source(venv_dir, args.pyqt_version) elif args.pyqt_type == 'link': diff --git a/scripts/opengl_info.py b/scripts/opengl_info.py index 5dc8f81c6..7c5ede6e7 100644 --- a/scripts/opengl_info.py +++ b/scripts/opengl_info.py @@ -18,8 +18,8 @@ """Show information about the OpenGL setup.""" -from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile, - QOffscreenSurface, QGuiApplication) +from PyQt6.QtGui import QOpenGLContext, QOffscreenSurface, QGuiApplication +from PyQt6.QtOpenGL import QOpenGLVersionProfile, QOpenGLVersionFunctionsFactory app = QGuiApplication([]) @@ -38,7 +38,7 @@ print(f"GLES: {ctx.isOpenGLES()}") vp = QOpenGLVersionProfile() vp.setVersion(2, 0) -vf = ctx.versionFunctions(vp) +vf = QOpenGLVersionFunctionsFactory.get(vp, ctx) print(f"Vendor: {vf.glGetString(vf.GL_VENDOR)}") print(f"Renderer: {vf.glGetString(vf.GL_RENDERER)}") print(f"Version: {vf.glGetString(vf.GL_VERSION)}") diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 62b409a19..aad8f2792 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -147,6 +147,14 @@ Feature: Searching on a page And I run :search-next Then the error "No search done yet." should be shown + # https://github.com/qutebrowser/qutebrowser/issues/7275 + @qtwebkit_skip + Scenario: Jumping to next without matches + When I run :search doesnotmatch + And I wait for the warning "Text 'doesnotmatch' not found on page!" + And I run :search-next + Then the warning "Text 'doesnotmatch' not found on page!" should be shown + Scenario: Repeating search in a second tab (issue #940) When I open data/search.html in a new tab And I run :search foo @@ -222,6 +230,14 @@ Feature: Searching on a page And I run :search-prev Then the error "No search done yet." should be shown + # https://github.com/qutebrowser/qutebrowser/issues/7275 + @qtwebkit_skip + Scenario: Jumping to previous without matches + When I run :search doesnotmatch + And I wait for the warning "Text 'doesnotmatch' not found on page!" + And I run :search-prev + Then the warning "Text 'doesnotmatch' not found on page!" should be shown + ## wrapping Scenario: Wrapping around page diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 56524a031..caa86dfbb 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -25,6 +25,7 @@ import importlib import re import json import platform +from contextlib import nullcontext as does_not_raise import pytest from qutebrowser.qt.core import QProcess, QPoint @@ -916,3 +917,15 @@ def test_sandboxing( status = dict(line.split("\t") for line in lines) assert status == expected_status + + +@pytest.mark.not_frozen +def test_logfilter_arg_does_not_crash(request, quteproc_new): + args = ['--temp-basedir', '--debug', '--logfilter', 'commands, init, ipc, webview'] + + with does_not_raise(): + quteproc_new.start(args=args + _base_args(request.config)) + + # Waiting for quit to make sure no other warning is emitted + quteproc_new.send_cmd(':quit') + quteproc_new.wait_for_quit() diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py index 047454e25..cd40af6e8 100644 --- a/tests/unit/browser/webkit/test_tabhistory.py +++ b/tests/unit/browser/webkit/test_tabhistory.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """Tests for webelement.tabhistory.""" import dataclasses @@ -26,7 +23,9 @@ from typing import Any import pytest pytest.importorskip('qutebrowser.qt.webkit') from qutebrowser.qt.core import QUrl, QPoint +# pylint: disable=no-name-in-module from qutebrowser.qt.webkit import QWebHistory +# pylint: enable=no-name-in-module from qutebrowser.browser.webkit import tabhistory from qutebrowser.misc.sessions import TabHistoryItem as Item diff --git a/tests/unit/javascript/test_js_execution.py b/tests/unit/javascript/test_js_execution.py index 542b56975..fd2469148 100644 --- a/tests/unit/javascript/test_js_execution.py +++ b/tests/unit/javascript/test_js_execution.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# FIXME:qt6 (lint) -# pylint: disable=no-name-in-module - """Check how Qt behaves when trying to execute JS.""" @@ -29,7 +26,7 @@ def test_simple_js_webkit(webview, js_enabled, expected): """With QtWebKit, evaluateJavaScript works when JS is on.""" # If we get there (because of the webview fixture) we can be certain # QtWebKit is available - from qutebrowser.qt.webkit import QWebSettings + from qutebrowser.qt.webkit import QWebSettings # pylint: disable=no-name-in-module webview.settings().setAttribute(QWebSettings.WebAttribute.JavascriptEnabled, js_enabled) result = webview.page().mainFrame().evaluateJavaScript('1 + 1') assert result == expected @@ -40,7 +37,7 @@ def test_element_js_webkit(webview, js_enabled, expected): """With QtWebKit, evaluateJavaScript on an element works with JS off.""" # If we get there (because of the webview fixture) we can be certain # QtWebKit is available - from qutebrowser.qt.webkit import QWebSettings + from qutebrowser.qt.webkit import QWebSettings # pylint: disable=no-name-in-module webview.settings().setAttribute(QWebSettings.WebAttribute.JavascriptEnabled, js_enabled) elem = webview.page().mainFrame().documentElement() result = elem.evaluateJavaScript('1 + 1') diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index 3826d3ee9..5f151704a 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -24,6 +24,7 @@ import dataclasses from typing import Optional from qutebrowser.qt.core import Qt +from qutebrowser.keyinput import keyutils @dataclasses.dataclass(order=True) @@ -606,7 +607,7 @@ KEYS = [ Key('unknown', 'Unknown', qtest=False), # 0x0 is used by Qt for unknown keys... - Key(attribute='', name='nil', member=0x0, qtest=False), + Key(attribute='', name='nil', member=keyutils._NIL_KEY, qtest=False), ] diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 52e0a01df..0ae0702e9 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -171,7 +171,7 @@ class TestHandle: assert not prompt_keyparser._count def test_invalid_key(self, prompt_keyparser): - keys = [Qt.Key.Key_B, 0x0] + keys = [Qt.Key.Key_B, keyutils._NIL_KEY] for key in keys: info = keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier) prompt_keyparser.handle(info.to_event()) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 2c0740c20..c3b6dc236 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -31,6 +31,16 @@ from qutebrowser.keyinput import keyutils from qutebrowser.utils import utils +pyqt_enum_workaround_skip = pytest.mark.skipif( + isinstance(keyutils._NIL_KEY, int), + reason="Can't create QKey for unknown keys with this PyQt version" +) +try: + OE_KEY = Qt.Key(ord('Œ')) +except ValueError: + OE_KEY = None # affected tests skipped + + @pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute) def qt_key(request): """Get all existing keys from key_data.py. @@ -156,10 +166,14 @@ class TestKeyToString: (Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier | Qt.KeyboardModifier.ShiftModifier, '<Meta+Ctrl+Alt+Shift+a>'), - (ord('Œ'), Qt.KeyboardModifier.NoModifier, '<Œ>'), - (ord('Œ'), Qt.KeyboardModifier.ShiftModifier, '<Shift+Œ>'), - (ord('Œ'), Qt.KeyboardModifier.GroupSwitchModifier, '<AltGr+Œ>'), - (ord('Œ'), Qt.KeyboardModifier.GroupSwitchModifier | Qt.KeyboardModifier.ShiftModifier, '<AltGr+Shift+Œ>'), + + pytest.param(OE_KEY, Qt.KeyboardModifier.NoModifier, '<Œ>', + marks=pyqt_enum_workaround_skip), + pytest.param(OE_KEY, Qt.KeyboardModifier.ShiftModifier, '<Shift+Œ>', + marks=pyqt_enum_workaround_skip), + pytest.param(OE_KEY, Qt.KeyboardModifier.GroupSwitchModifier, '<AltGr+Œ>', + marks=pyqt_enum_workaround_skip), + pytest.param(OE_KEY, Qt.KeyboardModifier.GroupSwitchModifier | Qt.KeyboardModifier.ShiftModifier, '<AltGr+Shift+Œ>'), (Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier, '<Shift>'), (Qt.Key.Key_Shift, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, '<Ctrl+Shift>'), @@ -212,10 +226,10 @@ def test_surrogates(key, modifiers, text, expected, pyqt_enum_workaround): ([Qt.Key.Key_Shift, 0x29df6], '<Shift><𩷶>'), ([0x1f468, 0x200d, 0x1f468, 0x200d, 0x1f466], '<👨><><👨><><👦>'), ]) -def test_surrogate_sequences(keys, expected, pyqt_enum_workaround): - infos = [keyutils.KeyInfo(key) for key in keys] - with pyqt_enum_workaround(keyutils.KeyParseError): - seq = keyutils.KeySequence(*infos) +@pyqt_enum_workaround_skip +def test_surrogate_sequences(keys, expected): + infos = [keyutils.KeyInfo(Qt.Key(key)) for key in keys] + seq = keyutils.KeySequence(*infos) assert str(seq) == expected @@ -590,7 +604,8 @@ def test_key_info_to_qt(): (Qt.Key.Key_Return, False), (Qt.Key.Key_Enter, False), (Qt.Key.Key_Space, False), - (0x0, False), # Used by Qt for unknown keys + # Used by Qt for unknown keys + pytest.param(keyutils._NIL_KEY, False, marks=pyqt_enum_workaround_skip), (Qt.Key.Key_ydiaeresis, True), (Qt.Key.Key_X, True), diff --git a/tests/unit/test_qt_machinery.py b/tests/unit/test_qt_machinery.py index 1618e9e6f..32f63043b 100644 --- a/tests/unit/test_qt_machinery.py +++ b/tests/unit/test_qt_machinery.py @@ -23,6 +23,7 @@ import html import argparse import typing from typing import Any, Optional, List, Dict, Union +import dataclasses import pytest @@ -214,7 +215,7 @@ def modules(): reason=machinery.SelectionReason.auto, outcomes={ "PyQt6": "ImportError: Fake ImportError for PyQt6.", - } + }, ), id="import-error", ), @@ -230,111 +231,157 @@ def test_autoselect( assert machinery._autoselect_wrapper() == expected -@pytest.mark.parametrize( - "args, env, expected", - [ - # Defaults with no overrides - ( - None, - None, - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.default +@dataclasses.dataclass +class SelectWrapperCase: + name: str + expected: machinery.SelectionInfo + args: Optional[argparse.Namespace] = None + env: Optional[str] = None + override: Optional[str] = None + + def __str__(self): + return self.name + + +class TestSelectWrapper: + @pytest.mark.parametrize( + "tc", + [ + # Only argument given + SelectWrapperCase( + "pyqt6-arg", + args=argparse.Namespace(qt_wrapper="PyQt6"), + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.cli + ), ), - ), - ( - argparse.Namespace(qt_wrapper=None), - None, - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.default + SelectWrapperCase( + "pyqt5-arg", + args=argparse.Namespace(qt_wrapper="PyQt5"), + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.cli + ), ), - ), - ( - argparse.Namespace(qt_wrapper=None), - "", - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.default + SelectWrapperCase( + "pyqt6-arg-empty-env", + args=argparse.Namespace(qt_wrapper="PyQt5"), + env="", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.cli + ), ), - ), - # Only argument given - ( - argparse.Namespace(qt_wrapper="PyQt6"), - None, - machinery.SelectionInfo( - wrapper="PyQt6", reason=machinery.SelectionReason.cli + # Only environment variable given + SelectWrapperCase( + "pyqt6-env", + env="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.env + ), ), - ), - ( - argparse.Namespace(qt_wrapper="PyQt5"), - None, - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.cli + SelectWrapperCase( + "pyqt5-env", + env="PyQt5", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.env + ), ), - ), - ( - argparse.Namespace(qt_wrapper="PyQt5"), - "", - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.cli + # Both given + SelectWrapperCase( + "pyqt5-arg-pyqt6-env", + args=argparse.Namespace(qt_wrapper="PyQt5"), + env="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.cli + ), ), - ), - # Only environment variable given - ( - None, - "PyQt6", - machinery.SelectionInfo( - wrapper="PyQt6", reason=machinery.SelectionReason.env + SelectWrapperCase( + "pyqt6-arg-pyqt5-env", + args=argparse.Namespace(qt_wrapper="PyQt6"), + env="PyQt5", + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.cli + ), ), - ), - ( - None, - "PyQt5", - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.env + SelectWrapperCase( + "pyqt6-arg-pyqt6-env", + args=argparse.Namespace(qt_wrapper="PyQt6"), + env="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.cli + ), ), - ), - # Both given - ( - argparse.Namespace(qt_wrapper="PyQt5"), - "PyQt6", - machinery.SelectionInfo( - wrapper="PyQt5", reason=machinery.SelectionReason.cli + # Override + SelectWrapperCase( + "override-only", + override="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt6", reason=machinery.SelectionReason.override + ), ), - ), - ( - argparse.Namespace(qt_wrapper="PyQt6"), - "PyQt5", - machinery.SelectionInfo( - wrapper="PyQt6", reason=machinery.SelectionReason.cli + SelectWrapperCase( + "override-arg", + args=argparse.Namespace(qt_wrapper="PyQt5"), + override="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.cli + ), ), - ), - ( - argparse.Namespace(qt_wrapper="PyQt6"), - "PyQt6", - machinery.SelectionInfo( - wrapper="PyQt6", reason=machinery.SelectionReason.cli + SelectWrapperCase( + "override-env", + env="PyQt5", + override="PyQt6", + expected=machinery.SelectionInfo( + wrapper="PyQt5", reason=machinery.SelectionReason.env + ), ), - ), - ], -) -def test_select_wrapper( - args: Optional[argparse.Namespace], - env: Optional[str], - expected: machinery.SelectionInfo, - monkeypatch: pytest.MonkeyPatch, - undo_init: None, -): - if env is None: - monkeypatch.delenv("QUTE_QT_WRAPPER", raising=False) - else: - monkeypatch.setenv("QUTE_QT_WRAPPER", env) + ], + ids=str, + ) + def test_select(self, tc: SelectWrapperCase, monkeypatch: pytest.MonkeyPatch): + if tc.env is None: + monkeypatch.delenv("QUTE_QT_WRAPPER", raising=False) + else: + monkeypatch.setenv("QUTE_QT_WRAPPER", tc.env) + + if tc.override is not None: + monkeypatch.setattr(machinery, "_WRAPPER_OVERRIDE", tc.override) + + assert machinery._select_wrapper(tc.args) == tc.expected + + @pytest.mark.parametrize( + "args, env", + [ + (None, None), + (argparse.Namespace(qt_wrapper=None), None), + (argparse.Namespace(qt_wrapper=None), ""), + ], + ) + def test_autoselect_by_default( + self, + args: Optional[argparse.Namespace], + env: Optional[str], + monkeypatch: pytest.MonkeyPatch, + ): + """Test that the default behavior is to autoselect a wrapper. + + Autoselection itself is tested further down. + """ + if env is None: + monkeypatch.delenv("QUTE_QT_WRAPPER", raising=False) + else: + monkeypatch.setenv("QUTE_QT_WRAPPER", env) - assert machinery._select_wrapper(args) == expected + assert machinery._select_wrapper(args).reason == machinery.SelectionReason.auto + def test_after_qt_import(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setitem(sys.modules, "PyQt6", None) + with pytest.warns(UserWarning, match="PyQt6 already imported"): + machinery._select_wrapper(args=None) -def test_select_wrapper_after_qt_import(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setitem(sys.modules, "PyQt6", None) - with pytest.warns(UserWarning, match="PyQt6 already imported"): - machinery._select_wrapper(args=None) + def test_invalid_override(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(machinery, "_WRAPPER_OVERRIDE", "invalid") + with pytest.raises(AssertionError): + machinery._select_wrapper(args=None) class TestInit: @@ -359,15 +406,34 @@ class TestInit: ): machinery.init(args=empty_args) + @pytest.fixture(params=["auto", "", None]) + def qt_auto_env( + self, + request: pytest.FixtureRequest, + monkeypatch: pytest.MonkeyPatch, + ): + """Trigger wrapper autoselection via environment variable. + + Autoselection should be used in three scenarios: + + - The environment variable is set to "auto". + - The environment variable is set to an empty string. + - The environment variable is not set at all. + + We run test_none_available_*() for all three scenarios. + """ + if request.param is None: + monkeypatch.delenv("QUTE_QT_WRAPPER", raising=False) + else: + monkeypatch.setenv("QUTE_QT_WRAPPER", request.param) + def test_none_available_implicit( self, stubs: Any, modules: Dict[str, bool], monkeypatch: pytest.MonkeyPatch, - undo_init: None, + qt_auto_env: None, ): - # FIXME:qt6 Also try without this once auto is default - monkeypatch.setenv("QUTE_QT_WRAPPER", "auto") stubs.ImportFake(modules, monkeypatch).patch() message_lines = [ @@ -391,10 +457,8 @@ class TestInit: modules: Dict[str, bool], monkeypatch: pytest.MonkeyPatch, empty_args: argparse.Namespace, - undo_init: None, + qt_auto_env: None, ): - # FIXME:qt6 Also try without this once auto is default - monkeypatch.setenv("QUTE_QT_WRAPPER", "auto") stubs.ImportFake(modules, monkeypatch).patch() info = machinery.init(args=empty_args) @@ -403,7 +467,7 @@ class TestInit: reason=machinery.SelectionReason.auto, outcomes={ "PyQt6": "ImportError: Fake ImportError for PyQt6.", - } + }, ) @pytest.mark.parametrize( @@ -422,7 +486,6 @@ class TestInit: true_vars: str, explicit: bool, empty_args: argparse.Namespace, - undo_init: None, ): info = machinery.SelectionInfo( wrapper=selected_wrapper, diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 51b014f81..6eb1c4e4f 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -22,11 +22,9 @@ import argparse import itertools import sys import warnings -import dataclasses import pytest import _pytest.logging # pylint: disable=import-private-name -from qutebrowser.qt import core as qtcore from qutebrowser import qutebrowser from qutebrowser.utils import log @@ -241,7 +239,7 @@ class TestInitLog: @pytest.fixture(autouse=True) def setup(self, mocker): - mocker.patch('qutebrowser.utils.log.qtcore.qInstallMessageHandler', + mocker.patch('qutebrowser.utils.qtlog.qtcore.qInstallMessageHandler', autospec=True) yield # Make sure logging is in a sensible default state @@ -342,35 +340,6 @@ class TestInitLog: assert log.console_filter.names == {'misc'} -class TestHideQtWarning: - - """Tests for hide_qt_warning/QtWarningFilter.""" - - @pytest.fixture - def qt_logger(self): - return logging.getLogger('qt-tests') - - def test_unfiltered(self, qt_logger, caplog): - with log.hide_qt_warning("World", 'qt-tests'): - with caplog.at_level(logging.WARNING, 'qt-tests'): - qt_logger.warning("Hello World") - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'WARNING' - assert record.message == "Hello World" - - @pytest.mark.parametrize('line', [ - "Hello", # exact match - "Hello World", # match at start of line - " Hello World ", # match with spaces - ]) - def test_filtered(self, qt_logger, caplog, line): - with log.hide_qt_warning("Hello", 'qt-tests'): - with caplog.at_level(logging.WARNING, 'qt-tests'): - qt_logger.warning(line) - assert not caplog.records - - @pytest.mark.parametrize('suffix, expected', [ ('', 'STUB: test_stub'), ('foo', 'STUB: test_stub (foo)'), @@ -405,27 +374,3 @@ def test_warning_still_errors(): # Mainly a sanity check after the tests messing with warnings above. with pytest.raises(UserWarning): warnings.warn("error", UserWarning) - - -class TestQtMessageHandler: - - @dataclasses.dataclass - class Context: - - """Fake QMessageLogContext.""" - - function: str = None - category: str = None - file: str = None - line: int = None - - @pytest.fixture(autouse=True) - def init_args(self): - parser = qutebrowser.get_argparser() - args = parser.parse_args([]) - log.init_log(args) - - def test_empty_message(self, caplog): - """Make sure there's no crash with an empty message.""" - log.qt_message_handler(qtcore.QtMsgType.QtDebugMsg, self.Context(), "") - assert caplog.messages == ["Logged empty message!"] diff --git a/tests/unit/utils/test_qtlog.py b/tests/unit/utils/test_qtlog.py new file mode 100644 index 000000000..3dd62b9a9 --- /dev/null +++ b/tests/unit/utils/test_qtlog.py @@ -0,0 +1,82 @@ +# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. + + +"""Tests for qutebrowser.utils.qtlog.""" + +import dataclasses +import logging + +import pytest + +from qutebrowser import qutebrowser +from qutebrowser.utils import qtlog + +from qutebrowser.qt import core as qtcore + + +class TestQtMessageHandler: + + @dataclasses.dataclass + class Context: + + """Fake QMessageLogContext.""" + + function: str = None + category: str = None + file: str = None + line: int = None + + @pytest.fixture(autouse=True) + def init_args(self): + parser = qutebrowser.get_argparser() + args = parser.parse_args([]) + qtlog.init(args) + + def test_empty_message(self, caplog): + """Make sure there's no crash with an empty message.""" + qtlog.qt_message_handler(qtcore.QtMsgType.QtDebugMsg, self.Context(), "") + assert caplog.messages == ["Logged empty message!"] + + +class TestHideQtWarning: + + """Tests for hide_qt_warning/QtWarningFilter.""" + + @pytest.fixture + def qt_logger(self): + return logging.getLogger('qt-tests') + + def test_unfiltered(self, qt_logger, caplog): + with qtlog.hide_qt_warning("World", 'qt-tests'): + with caplog.at_level(logging.WARNING, 'qt-tests'): + qt_logger.warning("Hello World") + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'WARNING' + assert record.message == "Hello World" + + @pytest.mark.parametrize('line', [ + "Hello", # exact match + "Hello World", # match at start of line + " Hello World ", # match with spaces + ]) + def test_filtered(self, qt_logger, caplog, line): + with qtlog.hide_qt_warning("Hello", 'qt-tests'): + with caplog.at_level(logging.WARNING, 'qt-tests'): + qt_logger.warning(line) + assert not caplog.records @@ -11,12 +11,12 @@ minversion = 3.20 [testenv] setenv = - PYTEST_QT_API=pyqt5 - QUTE_QT_WRAPPER=PyQt5 - pyqt{62,63,64,65}: PYTEST_QT_API=pyqt6 - pyqt{62,63,64,65}: QUTE_QT_WRAPPER=PyQt6 + PYTEST_QT_API=pyqt6 + QUTE_QT_WRAPPER=PyQt6 + pyqt{515,5152}: PYTEST_QT_API=pyqt5 + pyqt{515,5152}: QUTE_QT_WRAPPER=PyQt5 cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= - py312: VIRTUALENV_PIP=23.1.2 + py312: VIRTUALENV_PIP=23.2 py312: PIP_REQUIRE_VIRTUALENV=0 passenv = PYTHON @@ -56,10 +56,10 @@ commands = {envpython} -bb -m pytest {posargs:tests} cov: {envpython} scripts/dev/check_coverage.py {posargs} -[testenv:py-qt6] +[testenv:py-qt5] setenv = - PYTEST_QT_API=pyqt6 - QUTE_QT_WRAPPER=PyQt6 + PYTEST_QT_API=pyqt5 + QUTE_QT_WRAPPER=PyQt5 [testenv:bleeding] basepython = {env:PYTHON:python3} @@ -112,6 +112,7 @@ deps = -r{toxinidir}/misc/requirements/requirements-tests.txt -r{toxinidir}/misc/requirements/requirements-pylint.txt -r{toxinidir}/misc/requirements/requirements-pyqt.txt + -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt commands = {envpython} -m pylint scripts qutebrowser --output-format=colorized --reports=no {posargs} {envpython} scripts/dev/run_pylint_on_tests.py {toxinidir} --output-format=colorized --reports=no {posargs} @@ -180,19 +181,19 @@ commands = {envpython} scripts/dev/check_doc_changes.py {posargs} {envpython} scripts/asciidoc2html.py {posargs} -[testenv:pyinstaller-{64bit,32bit}{,-qt6}] +[testenv:pyinstaller-{64bit,32bit}{,-qt5}] basepython = {env:PYTHON:python3} passenv = APPDATA HOME PYINSTALLER_DEBUG setenv = - qt6: PYINSTALLER_QT6=true + qt5: PYINSTALLER_QT5=true deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt - !qt6: -r{toxinidir}/misc/requirements/requirements-pyqt.txt - qt6: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt + !qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt + qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt commands = {envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec @@ -225,6 +226,7 @@ deps = -r{toxinidir}/misc/requirements/requirements-tests.txt -r{toxinidir}/misc/requirements/requirements-mypy.txt pyqt6: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt +commands_pre = pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt6 commands = {envpython} -m mypy {env:QUTE_CONSTANTS_ARGS} qutebrowser {posargs} @@ -245,9 +247,7 @@ commands = basepython = {env:PYTHON:python3} passenv = {[testenv:mypy-pyqt6]passenv} deps = {[testenv:mypy-pyqt6]deps} -setenv = - pyqt6: QUTE_CONSTANTS_ARGS=--always-true=USE_PYQT6 --always-false=USE_PYQT5 --always-false=USE_PYSIDE2 --always-false=USE_PYSIDE6 --always-false=IS_QT5 --always-true=IS_QT6 - pyqt5: QUTE_CONSTANTS_ARGS=--always-false=USE_PYQT6 --always-true=USE_PYQT5 --always-false=USE_PYSIDE2 --always-false=USE_PYSIDE6 --always-true=IS_QT5 --always-false=IS_QT6 +setenv = {[testenv:mypy-pyqt6]setenv} commands = {envpython} -m mypy --cobertura-xml-report {envtmpdir} {env:QUTE_CONSTANTS_ARGS} qutebrowser tests {posargs} {envdir}/bin/diff-cover --fail-under=100 --compare-branch={env:DIFF_BRANCH:origin/{env:GITHUB_BASE_REF:master}} {envtmpdir}/cobertura.xml @@ -263,21 +263,21 @@ deps = commands = {envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/ -[testenv:build-release{,-qt6}] +[testenv:build-release{,-qt5}] basepython = {env:PYTHON:python3} passenv = * -# Override default PyQt5 from [testenv] +# Override default PyQt6 from [testenv] setenv = - qt6: QUTE_QT_WRAPPER=PyQt6 + qt5: QUTE_QT_WRAPPER=PyQt5 usedevelop = true deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-tox.txt -r{toxinidir}/misc/requirements/requirements-docs.txt - !qt6: -r{toxinidir}/misc/requirements/requirements-pyqt.txt - qt6: -r{toxinidir}/misc/requirements/requirements-pyqt-6.txt + !qt5: -r{toxinidir}/misc/requirements/requirements-pyqt.txt + qt5: -r{toxinidir}/misc/requirements/requirements-pyqt-5.txt -r{toxinidir}/misc/requirements/requirements-dev.txt -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt commands = - !qt6: {envpython} {toxinidir}/scripts/dev/build_release.py {posargs} - qt6: {envpython} {toxinidir}/scripts/dev/build_release.py --qt6 {posargs} + !qt5: {envpython} {toxinidir}/scripts/dev/build_release.py {posargs} + qt5: {envpython} {toxinidir}/scripts/dev/build_release.py --qt5 {posargs} |