summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml20
-rw-r--r--.github/workflows/docker.yml1
-rw-r--r--.github/workflows/nightly.yml40
-rw-r--r--.pylintrc6
-rw-r--r--doc/changelog.asciidoc14
-rw-r--r--misc/qutebrowser.spec2
-rw-r--r--misc/requirements/requirements-dev.txt16
-rw-r--r--misc/requirements/requirements-flake8.txt6
-rw-r--r--misc/requirements/requirements-mypy.txt11
-rw-r--r--misc/requirements/requirements-mypy.txt-raw1
-rw-r--r--misc/requirements/requirements-pyinstaller.txt2
-rw-r--r--misc/requirements/requirements-pylint.txt12
-rw-r--r--misc/requirements/requirements-pyqt.txt10
-rw-r--r--misc/requirements/requirements-pyqt.txt-raw6
-rw-r--r--misc/requirements/requirements-pyroma.txt6
-rw-r--r--misc/requirements/requirements-sphinx.txt8
-rw-r--r--misc/requirements/requirements-tests.txt20
-rw-r--r--misc/requirements/requirements-tox.txt12
-rw-r--r--misc/requirements/requirements-yamllint.txt2
-rwxr-xr-xmisc/userscripts/add-nextcloud-bookmarks4
-rwxr-xr-xmisc/userscripts/add-nextcloud-cookbook4
-rwxr-xr-xmisc/userscripts/qute-keepass8
-rw-r--r--qutebrowser/app.py12
-rw-r--r--qutebrowser/browser/browsertab.py10
-rw-r--r--qutebrowser/browser/commands.py3
-rw-r--r--qutebrowser/browser/downloadview.py10
-rw-r--r--qutebrowser/browser/history.py11
-rw-r--r--qutebrowser/browser/inspector.py9
-rw-r--r--qutebrowser/browser/network/pac.py8
-rw-r--r--qutebrowser/browser/network/proxy.py4
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py4
-rw-r--r--qutebrowser/browser/qutescheme.py7
-rw-r--r--qutebrowser/browser/webengine/interceptor.py5
-rw-r--r--qutebrowser/browser/webengine/notification.py7
-rw-r--r--qutebrowser/browser/webengine/tabhistory.py4
-rw-r--r--qutebrowser/browser/webengine/webenginequtescheme.py3
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py3
-rw-r--r--qutebrowser/browser/webengine/webview.py4
-rw-r--r--qutebrowser/browser/webkit/network/networkmanager.py4
-rw-r--r--qutebrowser/browser/webkit/webkitelem.py5
-rw-r--r--qutebrowser/browser/webkit/webkithistory.py5
-rw-r--r--qutebrowser/browser/webkit/webkitinspector.py4
-rw-r--r--qutebrowser/browser/webkit/webkitsettings.py5
-rw-r--r--qutebrowser/browser/webkit/webkittab.py4
-rw-r--r--qutebrowser/browser/webkit/webpage.py5
-rw-r--r--qutebrowser/browser/webkit/webview.py5
-rw-r--r--qutebrowser/completion/completiondelegate.py1
-rw-r--r--qutebrowser/completion/completionwidget.py40
-rw-r--r--qutebrowser/config/configfiles.py4
-rw-r--r--qutebrowser/html/warning-qt5.html28
-rw-r--r--qutebrowser/keyinput/eventfilter.py8
-rw-r--r--qutebrowser/keyinput/keyutils.py30
-rw-r--r--qutebrowser/keyinput/modeman.py6
-rw-r--r--qutebrowser/mainwindow/prompt.py3
-rw-r--r--qutebrowser/mainwindow/statusbar/command.py6
-rw-r--r--qutebrowser/mainwindow/statusbar/url.py4
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py31
-rw-r--r--qutebrowser/mainwindow/tabwidget.py28
-rw-r--r--qutebrowser/misc/consolewidget.py1
-rw-r--r--qutebrowser/misc/crashdialog.py3
-rw-r--r--qutebrowser/misc/earlyinit.py18
-rw-r--r--qutebrowser/misc/guiprocess.py4
-rw-r--r--qutebrowser/misc/httpclient.py8
-rw-r--r--qutebrowser/misc/ipc.py7
-rw-r--r--qutebrowser/misc/miscwidgets.py28
-rw-r--r--qutebrowser/misc/nativeeventfilter.py5
-rw-r--r--qutebrowser/misc/quitter.py7
-rw-r--r--qutebrowser/misc/split.py4
-rw-r--r--qutebrowser/qt/_core_pyqtproperty.py8
-rw-r--r--qutebrowser/qt/machinery.py43
-rw-r--r--qutebrowser/qt/opengl.py2
-rw-r--r--qutebrowser/qutebrowser.py2
-rw-r--r--qutebrowser/utils/debug.py1
-rw-r--r--qutebrowser/utils/log.py197
-rw-r--r--qutebrowser/utils/qtlog.py241
-rw-r--r--qutebrowser/utils/qtutils.py75
-rw-r--r--qutebrowser/utils/standarddir.py4
-rw-r--r--qutebrowser/utils/urlutils.py6
-rw-r--r--qutebrowser/utils/utils.py24
-rw-r--r--qutebrowser/utils/version.py9
-rw-r--r--requirements.txt6
-rwxr-xr-xscripts/dev/build_release.py62
-rw-r--r--scripts/dev/changelog_urls.json7
-rw-r--r--scripts/dev/misc_checks.py47
-rwxr-xr-xscripts/dev/run_vulture.py1
-rw-r--r--scripts/dev/standardpaths_tester.py2
-rw-r--r--scripts/keytester.py3
-rw-r--r--scripts/link_pyqt.py2
-rwxr-xr-xscripts/mkvenv.py13
-rw-r--r--scripts/opengl_info.py6
-rw-r--r--tests/end2end/features/search.feature16
-rw-r--r--tests/end2end/test_invocations.py13
-rw-r--r--tests/unit/browser/webkit/test_tabhistory.py5
-rw-r--r--tests/unit/javascript/test_js_execution.py7
-rw-r--r--tests/unit/keyinput/key_data.py3
-rw-r--r--tests/unit/keyinput/test_basekeyparser.py2
-rw-r--r--tests/unit/keyinput/test_keyutils.py33
-rw-r--r--tests/unit/test_qt_machinery.py263
-rw-r--r--tests/unit/utils/test_log.py57
-rw-r--r--tests/unit/utils/test_qtlog.py82
-rw-r--r--tox.ini44
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:
diff --git a/.pylintrc b/.pylintrc
index f89e3fa50..341bbe8cb 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -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
diff --git a/tox.ini b/tox.ini
index ffb15e2f9..74eb5213a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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}