diff options
71 files changed, 342 insertions, 185 deletions
@@ -70,6 +70,4 @@ per-file-ignores = copyright-check = True copyright-regexp = # Copyright [\d-]+ .* copyright-min-file-size = 110 -pytest-fixture-no-parentheses = True -pytest-mark-no-parentheses = True pytest-parametrize-names-type = csv diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 732ce4b07..599ba3b1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: run: "python scripts/dev/ci/problemmatchers.py ${{ matrix.testenv }} ${{ runner.temp }}" - name: Install dependencies run: | - [[ ${{ matrix.testenv }} == eslint ]] && npm install -g eslint + [[ ${{ matrix.testenv }} == eslint ]] && npm install -g 'eslint@<9.0.0' [[ ${{ 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a751591f..aa8b3b2ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -117,7 +117,7 @@ jobs: git push --set-upstream origin v${{ steps.bump.outputs.version_x }} - name: Create GitHub draft release id: create-release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.bump.outputs.version }} draft: true @@ -22,13 +22,15 @@ disallow_any_unimported = True enable_error_code = ignore-without-code ### Output -show_error_codes = True show_error_context = True pretty = True ### FIXME:v4 get rid of this no_implicit_optional = False +### Future default behavior +local_partial_types = True + [mypy-hunter] # https://github.com/ionelmc/python-hunter/issues/43 ignore_missing_imports = True diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index fce509dd7..b6385b015 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -35,6 +35,18 @@ Changed - A few more completions will now match search terms in any order: `:quickmark-*`, `:bookmark-*`, `:tab-take` and `:tab-select` (for the quick and bookmark categories). (#7955) +- Elements with an ARIA `role="switch"` now get hints (toggle switches like + e.g. on cookie banners). +- The `tor_identity` userscript now validates that the -c|--control-port + argument value is an int. (#8162) + +Fixed +~~~~~ + +- `input.insert_mode.auto_load` sometimes not triggering due to a race + condition. +- Worked around qutebrowser quitting when closing a KDE file dialog due to a Qt + bug. [[v3.1.1]] v3.1.1 (unreleased) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index b69700880..af76527c9 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -3476,6 +3476,7 @@ Default: * +pass:[[role="button"\]]+ * +pass:[[role="tab"\]]+ * +pass:[[role="checkbox"\]]+ +* +pass:[[role="switch"\]]+ * +pass:[[role="menuitem"\]]+ * +pass:[[role="menuitemcheckbox"\]]+ * +pass:[[role="menuitemradio"\]]+ diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index 08e332919..b0993ea58 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -1,9 +1,9 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -build==1.1.1 +build==1.2.1 check-manifest==0.49 -importlib-metadata==7.0.1 -packaging==23.2 +importlib_metadata==7.1.0 +packaging==24.0 pyproject_hooks==1.0.0 tomli==2.0.1 -zipp==3.17.0 +zipp==3.18.1 diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index f1ea191ba..59a1f8a03 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -1,6 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -build==1.1.1 +backports.tarfile==1.0.0 +build==1.2.1 bump2version==1.0.1 certifi==2024.2.2 cffi==1.16.0 @@ -9,25 +10,27 @@ cryptography==42.0.5 docutils==0.20.1 github3.py==4.0.1 hunter==3.6.1 -idna==3.6 -importlib-metadata==7.0.1 -importlib_resources==6.1.2 -jaraco.classes==3.3.1 +idna==3.7 +importlib_metadata==7.1.0 +importlib_resources==6.4.0 +jaraco.classes==3.4.0 +jaraco.context==5.3.0 +jaraco.functools==4.0.0 jeepney==0.8.0 -keyring==24.3.1 +keyring==25.1.0 manhole==1.8.0 markdown-it-py==3.0.0 mdurl==0.1.2 more-itertools==10.2.0 -nh3==0.2.15 -packaging==23.2 +nh3==0.2.17 +packaging==24.0 pkginfo==1.10.0 -pycparser==2.21 +pycparser==2.22 Pygments==2.17.2 PyJWT==2.8.0 Pympler==1.0.1 pyproject_hooks==1.0.0 -PyQt-builder==1.15.4 +PyQt-builder==1.16.0 python-dateutil==2.9.0.post0 readme_renderer==43.0 requests==2.31.0 @@ -39,7 +42,7 @@ sip==6.8.3 six==1.16.0 tomli==2.0.1 twine==5.0.0 -typing_extensions==4.10.0 +typing_extensions==4.11.0 uritemplate==4.1.1 # urllib3==2.2.1 -zipp==3.17.0 +zipp==3.18.1 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 4f9f7fb39..fa541e4a8 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -3,14 +3,14 @@ attrs==23.2.0 flake8==7.0.0 flake8-bugbear==24.2.6 -flake8-builtins==2.2.0 +flake8-builtins==2.5.0 flake8-comprehensions==3.14.0 flake8-debugger==4.1.2 flake8-deprecated==2.2.1 flake8-docstrings==1.7.0 flake8-future-import==0.4.7 flake8-plugin-utils==1.3.3 -flake8-pytest-style==1.7.2 +flake8-pytest-style==2.0.0 flake8-string-format==0.3.0 flake8-tidy-imports==4.10.0 flake8-tuple==0.4.1 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index cf2ffb667..9bb872d13 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,21 +1,21 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py chardet==5.2.0 -diff_cover==8.0.3 -importlib_resources==6.1.2 +diff_cover==9.0.0 +importlib_resources==6.4.0 Jinja2==3.1.3 -lxml==5.1.0 +lxml==5.2.1 MarkupSafe==2.1.5 -mypy==1.8.0 +mypy==1.9.0 mypy-extensions==1.0.0 pluggy==1.4.0 Pygments==2.17.2 PyQt5-stubs==5.15.6.0 tomli==2.0.1 -types-colorama==0.4.15.20240205 -types-docutils==0.20.0.20240304 -types-Pygments==2.17.0.20240106 -types-PyYAML==6.0.12.12 -types-setuptools==69.1.0.20240302 -typing_extensions==4.10.0 -zipp==3.17.0 +types-colorama==0.4.15.20240311 +types-docutils==0.20.0.20240406 +types-Pygments==2.17.0.20240310 +types-PyYAML==6.0.12.20240311 +types-setuptools==69.5.0.20240415 +typing_extensions==4.11.0 +zipp==3.18.1 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index fe0166b15..6c4b1d43b 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -1,8 +1,8 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py altgraph==0.17.4 -importlib-metadata==7.0.1 -packaging==23.2 -pyinstaller==6.4.0 -pyinstaller-hooks-contrib==2024.2 -zipp==3.17.0 +importlib_metadata==7.1.0 +packaging==24.0 +pyinstaller==6.6.0 +pyinstaller-hooks-contrib==2024.4 +zipp==3.18.1 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 2e86f9e30..94aac82a0 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -7,12 +7,12 @@ charset-normalizer==3.3.2 cryptography==42.0.5 dill==0.3.8 github3.py==4.0.1 -idna==3.6 +idna==3.7 isort==5.13.2 mccabe==0.7.0 pefile==2023.2.7 platformdirs==4.2.0 -pycparser==2.21 +pycparser==2.22 PyJWT==2.8.0 pylint==3.1.0 python-dateutil==2.9.0.post0 @@ -21,6 +21,6 @@ requests==2.31.0 six==1.16.0 tomli==2.0.1 tomlkit==0.12.4 -typing_extensions==4.10.0 +typing_extensions==4.11.0 uritemplate==4.1.1 # urllib3==2.2.1 diff --git a/misc/requirements/requirements-pyqt-6.6.txt b/misc/requirements/requirements-pyqt-6.6.txt index 9b9a11190..02f1a325f 100644 --- a/misc/requirements/requirements-pyqt-6.6.txt +++ b/misc/requirements/requirements-pyqt-6.6.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt6==6.6.1 -PyQt6-Qt6==6.6.2 +PyQt6-Qt6==6.6.3 PyQt6-sip==13.6.0 PyQt6-WebEngine==6.6.0 -PyQt6-WebEngine-Qt6==6.6.2 +PyQt6-WebEngine-Qt6==6.6.3 diff --git a/misc/requirements/requirements-pyqt-6.txt b/misc/requirements/requirements-pyqt-6.txt index 9b9a11190..02f1a325f 100644 --- a/misc/requirements/requirements-pyqt-6.txt +++ b/misc/requirements/requirements-pyqt-6.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt6==6.6.1 -PyQt6-Qt6==6.6.2 +PyQt6-Qt6==6.6.3 PyQt6-sip==13.6.0 PyQt6-WebEngine==6.6.0 -PyQt6-WebEngine-Qt6==6.6.2 +PyQt6-WebEngine-Qt6==6.6.3 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 9b9a11190..02f1a325f 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 PyQt6==6.6.1 -PyQt6-Qt6==6.6.2 +PyQt6-Qt6==6.6.3 PyQt6-sip==13.6.0 PyQt6-WebEngine==6.6.0 -PyQt6-WebEngine-Qt6==6.6.2 +PyQt6-WebEngine-Qt6==6.6.3 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index b5a1f35b7..141faf1cb 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -1,17 +1,17 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -build==1.1.1 +build==1.2.1 certifi==2024.2.2 charset-normalizer==3.3.2 docutils==0.20.1 -idna==3.6 -importlib-metadata==7.0.1 -packaging==23.2 +idna==3.7 +importlib_metadata==7.1.0 +packaging==24.0 Pygments==2.17.2 pyproject_hooks==1.0.0 pyroma==4.2 requests==2.31.0 tomli==2.0.1 -trove-classifiers==2024.3.3 +trove-classifiers==2024.4.10 urllib3==2.2.1 -zipp==3.17.0 +zipp==3.18.1 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 7a09b7815..7bb66c6a0 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -5,12 +5,12 @@ Babel==2.14.0 certifi==2024.2.2 charset-normalizer==3.3.2 docutils==0.20.1 -idna==3.6 +idna==3.7 imagesize==1.4.1 -importlib-metadata==7.0.1 +importlib_metadata==7.1.0 Jinja2==3.1.3 MarkupSafe==2.1.5 -packaging==23.2 +packaging==24.0 Pygments==2.17.2 pytz==2024.1 requests==2.31.0 @@ -23,4 +23,4 @@ sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 urllib3==2.2.1 -zipp==3.17.0 +zipp==3.18.1 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index e02bba064..0d8aa9bc4 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -7,38 +7,38 @@ certifi==2024.2.2 charset-normalizer==3.3.2 cheroot==10.0.0 click==8.1.7 -coverage==7.4.3 +coverage==7.4.4 exceptiongroup==1.2.0 -execnet==2.0.2 -filelock==3.13.1 -Flask==3.0.2 +execnet==2.1.1 +filelock==3.13.4 +Flask==3.0.3 hunter==3.6.1 -hypothesis==6.98.17 -idna==3.6 -importlib-metadata==7.0.1 +hypothesis==6.100.1 +idna==3.7 +importlib_metadata==7.1.0 iniconfig==2.0.0 itsdangerous==2.1.2 jaraco.functools==4.0.0 # Jinja2==3.1.3 -Mako==1.3.2 +Mako==1.3.3 manhole==1.8.0 # MarkupSafe==2.1.5 more-itertools==10.2.0 -packaging==23.2 +packaging==24.0 parse==1.20.1 parse-type==0.6.2 pluggy==1.4.0 py-cpuinfo==9.0.0 Pygments==2.17.2 -pytest==8.0.2 -pytest-bdd==7.1.1 +pytest==8.1.1 +pytest-bdd==7.1.2 pytest-benchmark==4.0.0 -pytest-cov==4.1.0 +pytest-cov==5.0.0 pytest-instafail==0.5.0 -pytest-mock==3.12.0 +pytest-mock==3.14.0 pytest-qt==4.4.0 pytest-repeat==0.9.3 -pytest-rerunfailures==13.0 +pytest-rerunfailures==14.0 pytest-xdist==3.5.0 pytest-xvfb==3.0.0 PyVirtualDisplay==3.0 @@ -47,10 +47,10 @@ requests-file==2.0.0 six==1.16.0 sortedcontainers==2.4.0 soupsieve==2.5 -tldextract==5.1.1 +tldextract==5.1.2 tomli==2.0.1 -typing_extensions==4.10.0 +typing_extensions==4.11.0 urllib3==2.2.1 vulture==2.11 -Werkzeug==3.0.1 -zipp==3.17.0 +Werkzeug==3.0.2 +zipp==3.18.1 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index f5dba2920..afea097d0 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -4,14 +4,14 @@ cachetools==5.3.3 chardet==5.2.0 colorama==0.4.6 distlib==0.3.8 -filelock==3.13.1 -packaging==23.2 +filelock==3.13.4 +packaging==24.0 pip==24.0 platformdirs==4.2.0 pluggy==1.4.0 pyproject-api==1.6.1 -setuptools==69.1.1 +setuptools==69.5.1 tomli==2.0.1 -tox==4.13.0 +tox==4.14.2 virtualenv==20.25.1 -wheel==0.42.0 +wheel==0.43.0 diff --git a/misc/userscripts/tor_identity b/misc/userscripts/tor_identity index 6b3828ed4..a6d7c9250 100755 --- a/misc/userscripts/tor_identity +++ b/misc/userscripts/tor_identity @@ -32,7 +32,7 @@ except ImportError: if __name__ == '__main__': parser = ArgumentParser(prog='tor_identity') - parser.add_argument('-c', '--control-port', default=9051, + parser.add_argument('-c', '--control-port', type=int, default=9051, help='Tor control port (default 9051).') parser.add_argument('-p', '--password', type=str, default=None, help='Tor control port password.') diff --git a/pytest.ini b/pytest.ini index 5c4c2095a..2de880eae 100644 --- a/pytest.ini +++ b/pytest.ini @@ -64,6 +64,12 @@ qt_log_ignore = # Seen in Qt 6.6.2 on CI, https://github.com/qutebrowser/qutebrowser/pull/8106#issuecomment-1952320663 ^QDBusConnection: couldn't handle call to Notify, no slot matched ^QDBusConnection: couldn't handle call to CloseNotification, no slot matched + # Qt 6.7 + ^Path override failed for key base::DIR_APP_DICTIONARIES and path '.*/qtwebengine_dictionaries' + # Sometime the above message gets printed twice at the same time and the messages get interleaved. + # The last part of the outer message gets bumped down to a line on its own, so hopefully this + # catches that. And we don't see any other weird permutations of this. + ^[^ ]*qtwebengine_dictionaries'$ xfail_strict = true filterwarnings = error diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 015715eef..51603a2b9 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -132,6 +132,9 @@ def init(*, args: argparse.Namespace) -> None: crashsignal.crash_handler.init_faulthandler() objects.qapp.setQuitOnLastWindowClosed(False) + # WORKAROUND for KDE file dialogs / QEventLoopLocker quitting: + # https://bugreports.qt.io/browse/QTBUG-124386 + objects.qapp.setQuitLockEnabled(False) quitter.instance.shutting_down.connect(QApplication.closeAllWindows) _init_icon() diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 4d14c9cd7..625046a9c 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -14,7 +14,7 @@ from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional from qutebrowser.qt import machinery from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt, - QEvent, QPoint, QRect) + QEvent, QPoint, QRect, QTimer) from qutebrowser.qt.gui import QKeyEvent, QIcon, QPixmap from qutebrowser.qt.widgets import QApplication, QWidget from qutebrowser.qt.printsupport import QPrintDialog, QPrinter @@ -902,7 +902,13 @@ class AbstractTabPrivate: modeman.enter(self._tab.win_id, usertypes.KeyMode.insert, 'load finished', only_if_normal=True) - self._tab.elements.find_focused(_auto_insert_mode_cb) + # There seems to be a race between loadFinished being called, + # and the autoload attribute on websites actually focusing anything. + # Thus, we delay this by a bit. Locally, a delay of 13ms caused no races + # with 5000 test reruns (even with simultaneous CPU stress testing), + # so 65ms should be a safe bet and still not be too noticeable. + QTimer.singleShot( + 65, lambda: self._tab.elements.find_focused(_auto_insert_mode_cb)) def clear_ssl_errors(self) -> None: raise NotImplementedError diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index f0c511853..62872d68e 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -4,6 +4,8 @@ """Handling of proxies.""" +from typing import Optional + from qutebrowser.qt.core import QUrl, pyqtSlot from qutebrowser.qt.network import QNetworkProxy, QNetworkProxyFactory @@ -13,7 +15,7 @@ from qutebrowser.misc import objects from qutebrowser.browser.network import pac -application_factory = None +application_factory: Optional["ProxyFactory"] = None def init(): diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 9dd507ab5..0360eed66 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -19,7 +19,7 @@ from qutebrowser.config import config, websettings 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 +from qutebrowser.browser.webkit import httpheaders from qutebrowser.browser.webkit.network import networkmanager @@ -533,7 +533,7 @@ class DownloadManager(downloads.AbstractDownloadManager): try: suggested_filename = target.suggested_filename() except downloads.NoFilenameError: - _, suggested_filename = http.parse_content_disposition(reply) + _, suggested_filename = httpheaders.parse_content_disposition(reply) log.downloads.debug("fetch: {} -> {}".format(reply.url(), suggested_filename)) download = DownloadItem(reply, manager=self) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index f325ff9e3..508d510d7 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -32,10 +32,10 @@ from qutebrowser.qt import sip pyeval_output = ":pyeval was never called" -csrf_token = None +csrf_token: Optional[str] = None -_HANDLERS = {} +_HANDLERS: Dict[str, "_HandlerCallable"] = {} class Error(Exception): diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 2b375a7b0..78a4946ad 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -37,7 +37,7 @@ private_profile: Optional[QWebEngineProfile] = None # The global WebEngineSettings object _global_settings = cast('WebEngineSettings', None) -parsed_user_agent = None +parsed_user_agent: Optional[websettings.UserAgent] = None _qute_scheme_handler = cast(webenginequtescheme.QuteSchemeHandler, None) _req_interceptor = cast('interceptor.RequestInterceptor', None) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 1c712db5e..02d912a50 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -292,6 +292,8 @@ class WebEngineCaret(browsertab.AbstractCaret): flags = set() if utils.is_windows: flags.add('windows') + if 'caret' in objects.debug_flags: + flags.add('debug') return list(flags) @pyqtSlot(usertypes.KeyMode) diff --git a/qutebrowser/browser/webkit/http.py b/qutebrowser/browser/webkit/httpheaders.py index 95b7b7104..95b7b7104 100644 --- a/qutebrowser/browser/webkit/http.py +++ b/qutebrowser/browser/webkit/httpheaders.py diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index ea19174ec..595432dc9 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -18,7 +18,7 @@ from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame from qutebrowser.config import websettings, config from qutebrowser.browser import pdfjs, shared, downloads, greasemonkey -from qutebrowser.browser.webkit import http +from qutebrowser.browser.webkit import httpheaders from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.utils import message, usertypes, log, jinja, objreg from qutebrowser.qt import sip @@ -263,14 +263,14 @@ class BrowserPage(QWebPage): At some point we might want to implement the MIME Sniffing standard here: https://mimesniff.spec.whatwg.org/ """ - inline, suggested_filename = http.parse_content_disposition(reply) + inline, suggested_filename = httpheaders.parse_content_disposition(reply) download_manager = objreg.get('qtnetwork-download-manager') if not inline: # Content-Disposition: attachment -> force download download_manager.fetch(reply, suggested_filename=suggested_filename) return - mimetype, _rest = http.parse_content_type(reply) + mimetype, _rest = httpheaders.parse_content_type(reply) if mimetype == 'image/jpg': # Some servers (e.g. the LinkedIn CDN) send a non-standard # image/jpg (instead of image/jpeg, defined in RFC 1341 section diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 1ad563c5d..0d63d0021 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -7,7 +7,7 @@ import traceback import re import contextlib -from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping +from typing import TYPE_CHECKING, Callable, Dict, Tuple, Iterator, Mapping, MutableMapping from qutebrowser.qt.core import pyqtSlot, QUrl, QObject @@ -21,7 +21,7 @@ if TYPE_CHECKING: _ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str] -last_command = {} +last_command: Dict[usertypes.KeyMode, Tuple[str, int]] = {} def _url(tabbed_browser): diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 573f25fa1..cb7fe77b3 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -29,7 +29,7 @@ key_instance = cast('KeyConfig', None) cache = cast('configcache.ConfigCache', None) # Keeping track of all change filters to validate them later. -change_filters = [] +change_filters: List["change_filter"] = [] # Sentinel UNSET = object() diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index de6948150..ca92f96c1 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1784,6 +1784,7 @@ hints.selectors: - '[role="button"]' - '[role="tab"]' - '[role="checkbox"]' + - '[role="switch"]' - '[role="menuitem"]' - '[role="menuitemcheckbox"]' - '[role="menuitemradio"]' diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 5d000c3ec..a08ddb619 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -7,6 +7,7 @@ import argparse import os.path import sys +from typing import Optional from qutebrowser.qt.widgets import QMessageBox @@ -19,7 +20,7 @@ from qutebrowser.misc import msgbox, objects, savemanager # Error which happened during init, so we can show a message box. -_init_errors = None +_init_errors: Optional[configexc.ConfigFileErrors] = None def early_init(args: argparse.Namespace) -> None: diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 9f5bf92d0..7824ae258 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -246,7 +246,7 @@ def clear_private_data() -> None: elif objects.backend == usertypes.Backend.QtWebKit: from qutebrowser.browser.webkit import cookies assert cookies.ram_cookie_jar is not None - cookies.ram_cookie_jar.setAllCookies([]) # type: ignore[unreachable] + cookies.ram_cookie_jar.setAllCookies([]) else: raise utils.Unreachable(objects.backend) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index b5b232c5a..ff9974d9d 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -21,7 +21,7 @@ from qutebrowser.misc import objects # ModuleInfo objects for all loaded plugins -_module_infos = [] +_module_infos: List["ModuleInfo"] = [] InitHookType = Callable[['InitContext'], None] ConfigChangedHookType = Callable[[], None] diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js index b16a15348..4aeefcdb9 100644 --- a/qutebrowser/javascript/caret.js +++ b/qutebrowser/javascript/caret.js @@ -740,6 +740,12 @@ window._qutebrowser.caret = (function() { CaretBrowsing.isWindows = null; /** + * Whether we should log debug outputs. + * @type {boolean} + */ + CaretBrowsing.isDebug = null; + + /** * The id returned by window.setInterval for our stopAnimation function, so * we can cancel it when we call stopAnimation again. * @type {number?} @@ -1150,6 +1156,8 @@ window._qutebrowser.caret = (function() { action = "extend"; } + CaretBrowsing.debug(`(move) ${action} ${count} ${granularity} ${direction}, selection ${CaretBrowsing.selectionState}`); + for (let i = 0; i < count; i++) { if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) { CaretBrowsing.updateLineSelection(direction, granularity); @@ -1180,6 +1188,8 @@ window._qutebrowser.caret = (function() { if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) { action = "extend"; } + CaretBrowsing.debug(`(moveToBlock) ${action} paragraph ${paragraph}, boundary ${boundary}, count ${count}, selection ${CaretBrowsing.selectionState}`); + for (let i = 0; i < count; i++) { window. getSelection(). @@ -1196,6 +1206,7 @@ window._qutebrowser.caret = (function() { }; CaretBrowsing.toggle = function(value) { + CaretBrowsing.debug(`(toggle) enabled ${CaretBrowsing.isEnabled}, force ${CaretBrowsing.forceEnabled}`); if (CaretBrowsing.forceEnabled) { CaretBrowsing.recreateCaretElement(); return; @@ -1231,6 +1242,7 @@ window._qutebrowser.caret = (function() { * is enabled and whether this window / iframe has focus. */ CaretBrowsing.updateIsCaretVisible = function() { + CaretBrowsing.debug(`(updateIsCaretVisible) isEnabled ${CaretBrowsing.isEnabled}, isWindowFocused ${CaretBrowsing.isWindowFocused}, isCaretVisible ${CaretBrowsing.isCaretVisible}, caretElement ${CaretBrowsing.caretElement}`); CaretBrowsing.isCaretVisible = (CaretBrowsing.isEnabled && CaretBrowsing.isWindowFocused); if (CaretBrowsing.isCaretVisible && !CaretBrowsing.caretElement) { @@ -1274,6 +1286,12 @@ window._qutebrowser.caret = (function() { } }; + CaretBrowsing.debug = (text) => { + if (CaretBrowsing.isDebug) { + console.debug(`caret: ${text}`); + } + } + CaretBrowsing.init = function() { CaretBrowsing.isWindowFocused = document.hasFocus(); @@ -1313,6 +1331,7 @@ window._qutebrowser.caret = (function() { funcs.setFlags = (flags) => { CaretBrowsing.isWindows = flags.includes("windows"); + CaretBrowsing.isDebug = flags.includes("debug"); }; funcs.disableCaret = () => { diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index af19bb61c..54b6e88b1 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -26,7 +26,7 @@ from qutebrowser.qt.gui import QKeySequence, QKeyEvent if machinery.IS_QT6: from qutebrowser.qt.core import QKeyCombination else: - QKeyCombination = None # QKeyCombination was added in Qt 6 + QKeyCombination: None = None # QKeyCombination was added in Qt 6 from qutebrowser.utils import utils, qtutils, debug diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index b489aaf36..08f5dc5ff 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -6,7 +6,7 @@ import sys import code -from typing import MutableSequence +from typing import MutableSequence, Optional from qutebrowser.qt.core import pyqtSignal, pyqtSlot, Qt from qutebrowser.qt.widgets import QTextEdit, QWidget, QVBoxLayout, QApplication @@ -17,7 +17,7 @@ from qutebrowser.misc import cmdhistory, miscwidgets from qutebrowser.utils import utils, objreg -console_widget = None +console_widget: Optional["ConsoleWidget"] = None class ConsoleLineEdit(miscwidgets.CommandLineEdit): diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index 2dc34c886..21a3352d6 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -28,7 +28,7 @@ PROTOCOL_VERSION = 1 # The ipc server instance -server = None +server: Optional["IPCServer"] = None def _get_socketname_windows(basedir): diff --git a/qutebrowser/misc/nativeeventfilter.py b/qutebrowser/misc/nativeeventfilter.py index 9b1bbb97c..06533bd42 100644 --- a/qutebrowser/misc/nativeeventfilter.py +++ b/qutebrowser/misc/nativeeventfilter.py @@ -20,7 +20,7 @@ from qutebrowser.utils import log # Needs to be saved to avoid garbage collection -_instance = None +_instance: Optional["NativeEventFilter"] = None # Using C-style naming for C structures in this file # pylint: disable=invalid-name diff --git a/qutebrowser/qt/machinery.py b/qutebrowser/qt/machinery.py index 616c7ccfc..9f45dd6ce 100644 --- a/qutebrowser/qt/machinery.py +++ b/qutebrowser/qt/machinery.py @@ -34,7 +34,7 @@ from qutebrowser.utils import log # 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. -_WRAPPER_OVERRIDE = None +_WRAPPER_OVERRIDE = None # type: ignore[var-annotated] WRAPPERS = [ "PyQt6", @@ -168,9 +168,9 @@ def _select_wrapper(args: Optional[argparse.Namespace]) -> SelectionInfo: - Otherwise, try the wrappers in WRAPPER in order (PyQt6 -> PyQt5) """ # If any Qt wrapper has been imported before this, something strange might - # be happening. + # be happening. With PyInstaller, it imports the Qt bindings early. for name in WRAPPERS: - if name in sys.modules: + if name in sys.modules and not hasattr(sys, "frozen"): warnings.warn(f"{name} already imported", stacklevel=1) if args is not None and args.qt_wrapper is not None: @@ -190,7 +190,7 @@ def _select_wrapper(args: Optional[argparse.Namespace]) -> SelectionInfo: return SelectionInfo(wrapper=env_wrapper, reason=SelectionReason.env) if _WRAPPER_OVERRIDE is not None: - assert _WRAPPER_OVERRIDE in WRAPPERS # type: ignore[unreachable] + assert _WRAPPER_OVERRIDE in WRAPPERS return SelectionInfo(wrapper=_WRAPPER_OVERRIDE, reason=SelectionReason.override) return _autoselect_wrapper() diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index c3a0a60be..e68156759 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -171,12 +171,13 @@ def debug_flag_error(flag): avoid-chromium-init: Enable `--version` without initializing Chromium. werror: Turn Python warnings into errors. test-notification-service: Use the testing libnotify service. + caret: Enable debug logging for caret.js. """ valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history', 'no-scroll-filtering', 'log-requests', 'log-cookies', 'log-scroll-pos', 'log-sensitive-keys', 'stack', 'chromium', 'wait-renderer-process', 'avoid-chromium-init', 'werror', - 'test-notification-service', 'log-qt-events'] + 'test-notification-service', 'log-qt-events', 'caret'] if flag in valid_flags: return flag diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 3e3b407b0..9695ec5a2 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from qutebrowser.config import config as configmodule _log_inited = False -_args = None +_args: Optional[argparse.Namespace] = None COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white'] COLOR_ESCAPES = {color: '\033[{}m'.format(i) @@ -146,7 +146,7 @@ LOGGER_NAMES = [ ram_handler: Optional['RAMHandler'] = None console_handler: Optional[logging.Handler] = None -console_filter = None +console_filter: Optional["LogFilter"] = None def stub(suffix: str = '') -> None: diff --git a/qutebrowser/utils/qtlog.py b/qutebrowser/utils/qtlog.py index 1de9181cf..78b48ebee 100644 --- a/qutebrowser/utils/qtlog.py +++ b/qutebrowser/utils/qtlog.py @@ -15,7 +15,7 @@ from typing import Iterator, Optional from qutebrowser.qt import core as qtcore from qutebrowser.utils import log -_args = None +_args: Optional[argparse.Namespace] = None def init(args: argparse.Namespace) -> None: diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 89175ca4e..21f3b8478 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -193,6 +193,15 @@ def check_qdatastream(stream: QDataStream) -> None: QDataStream.Status.WriteFailed: ("The data stream cannot write to the " "underlying device."), } + try: + status_to_str[QDataStream.Status.SizeLimitExceeded] = ( # type: ignore[attr-defined] + "The data stream cannot read or write the data because its size is larger " + "than supported by the current platform." + ) + except AttributeError: + # Added in Qt 6.7 + pass + if stream.status() != QDataStream.Status.Ok: raise OSError(status_to_str[stream.status()]) @@ -736,4 +745,4 @@ else: def add_optional(obj: Optional[_T]) -> Optional[_T]: return obj - QT_NONE = None + QT_NONE: None = None diff --git a/qutebrowser/utils/resources.py b/qutebrowser/utils/resources.py index 60d90fd31..a40f9d2bd 100644 --- a/qutebrowser/utils/resources.py +++ b/qutebrowser/utils/resources.py @@ -9,7 +9,7 @@ import sys import contextlib import posixpath import pathlib -from typing import Iterator, Iterable, Union +from typing import Iterator, Iterable, Union, Dict # We cannot use the stdlib version on 3.8 because we need the files() API. @@ -25,7 +25,7 @@ else: # pragma: no cover from importlib_resources.abc import Traversable import qutebrowser -_cache = {} +_cache: Dict[str, str] = {} _ResourceType = Union[Traversable, pathlib.Path] diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 2d2ae53f9..026376dc2 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -10,7 +10,7 @@ import sys import contextlib import enum import argparse -from typing import Iterator, Optional +from typing import Iterator, Optional, Dict from qutebrowser.qt.core import QStandardPaths from qutebrowser.qt.widgets import QApplication @@ -18,7 +18,7 @@ from qutebrowser.qt.widgets import QApplication from qutebrowser.utils import log, debug, utils, version, qtutils # The cached locations -_locations = {} +_locations: Dict["_Location", str] = {} class _Location(enum.Enum): diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index a9de4bc00..11c160c9e 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -38,7 +38,7 @@ except ImportError: # pragma: no cover from qutebrowser.utils import log -fake_clipboard = None +fake_clipboard: Optional[str] = None log_clipboard = False is_mac = sys.platform.startswith('darwin') diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 30aeedd20..32d5357db 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -78,7 +78,7 @@ class DistributionInfo: pretty: str -pastebin_url = None +pastebin_url: Optional[str] = None class Distribution(enum.Enum): @@ -611,12 +611,21 @@ class WebEngineVersions: # 6.5.0: Security fixes up to 110.0.5481.104 (2023-02-16) # 6.5.1: Security fixes up to 112.0.5615.138 (2023-04-18) # 6.5.2: Security fixes up to 114.0.5735.133 (2023-06-13) + # 6.5.3: Security fixes up to 117.0.5938.63 (2023-09-12) utils.VersionNumber(6, 5): '108.0.5359.220', # Qt 6.6: Chromium 112 # 112.0.5615.213 (~2023-04-18) - # 6.6.0: Security fixes up to 116.0.5845.110 (?) (2023-08-22) + # 6.6.0: Security fixes up to 117.0.5938.63 (2023-09-12) + # 6.6.1: Security fixes up to 119.0.6045.123 (2023-11-07) + # 6.6.2: Security fixes up to 121.0.6167.160 (2024-02-06) + # 6.6.3: Security fixes up to 122.0.6261.128 (2024-03-12) utils.VersionNumber(6, 6): '112.0.5615.213', + + # Qt 6.7: Chromium 118 + # 118.0.5993.220 (~2023-10-24) + # 6.6.0: Security fixes up to 122.0.6261.128 (?) (2024-03-12) + utils.VersionNumber(6, 7): '118.0.5993.220', } def __post_init__(self) -> None: diff --git a/requirements.txt b/requirements.txt index e4b19d98b..229f9e9c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,12 +2,12 @@ adblock==0.6.0 colorama==0.4.6 -importlib_resources==6.1.2 ; python_version=="3.8.*" +importlib_resources==6.4.0 ; python_version=="3.8.*" Jinja2==3.1.3 MarkupSafe==2.1.5 Pygments==2.17.2 PyYAML==6.0.1 -zipp==3.17.0 +zipp==3.18.1 # 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 65eef720c..40cedc2e8 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -171,6 +171,9 @@ def smoke_test(executable: pathlib.Path, debug: bool, qt5: bool) -> None: r'[0-9:]* WARNING: Qt WebEngine resources not found at .*', (r'[0-9:]* WARNING: Installed Qt WebEngine locales directory not found at ' r'location /qtwebengine_locales\. Trying application directory\.\.\.'), + # Qt 6.7, only seen on macos for some reason + (r'.*Path override failed for key base::DIR_APP_DICTIONARIES ' + r"and path '.*/qtwebengine_dictionaries'"), ]) elif IS_WINDOWS: stderr_whitelist.extend([ diff --git a/scripts/dev/changelog_urls.json b/scripts/dev/changelog_urls.json index 161e0a1cd..645bb6385 100644 --- a/scripts/dev/changelog_urls.json +++ b/scripts/dev/changelog_urls.json @@ -78,7 +78,6 @@ "sphinxcontrib-jsmath": "https://www.sphinx-doc.org/en/master/changes.html", "sphinxcontrib-qthelp": "https://www.sphinx-doc.org/en/master/changes.html", "sphinxcontrib-serializinghtml": "https://www.sphinx-doc.org/en/master/changes.html", - "jaraco.functools": "https://jaracofunctools.readthedocs.io/en/latest/history.html", "parse": "https://github.com/r1chardj0n3s/parse#potential-gotchas", "Pympler": "https://github.com/pympler/pympler/blob/master/CHANGELOG.md", "pytest-mock": "https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst", @@ -98,7 +97,7 @@ "PyQt5-Qt5": "https://www.riverbankcomputing.com/news", "PyQtWebEngine": "https://www.riverbankcomputing.com/news", "PyQtWebEngine-Qt5": "https://www.riverbankcomputing.com/news", - "PyQt-builder": "https://www.riverbankcomputing.com/news", + "PyQt-builder": "https://pyqt-builder.readthedocs.io/en/stable/releases.html", "PyQt5-sip": "https://www.riverbankcomputing.com/news", "PyQt5-stubs": "https://github.com/python-qt-tools/PyQt5-stubs/blob/master/CHANGELOG.md", "sip": "https://www.riverbankcomputing.com/news", @@ -126,7 +125,7 @@ "filelock": "https://github.com/tox-dev/py-filelock/releases", "github3.py": "https://github3.readthedocs.io/en/latest/release-notes/index.html", "manhole": "https://github.com/ionelmc/python-manhole/blob/master/CHANGELOG.rst", - "pycparser": "https://github.com/eliben/pycparser/blob/master/CHANGES", + "pycparser": "https://github.com/eliben/pycparser/releases", "python-dateutil": "https://dateutil.readthedocs.io/en/stable/changelog.html", "platformdirs": "https://github.com/platformdirs/platformdirs/releases", "pluggy": "https://github.com/pytest-dev/pluggy/blob/main/CHANGELOG.rst", @@ -134,16 +133,19 @@ "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/NEWS.rst", - "zipp": "https://github.com/jaraco/zipp/blob/main/NEWS.rst", + "importlib_metadata": "https://github.com/python/importlib_metadata/blob/main/NEWS.rst", + "zipp": "https://zipp.readthedocs.io/en/latest/history.html", "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", "pefile": "https://github.com/erocarrera/pefile/commits/master", "SecretStorage": "https://github.com/mitya57/secretstorage/blob/master/changelog", "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/NEWS.rst", + "keyring": "https://keyring.readthedocs.io/en/latest/history.html", + "jaraco.classes": "https://jaracoclasses.readthedocs.io/en/latest/history.html", + "jaraco.context": "https://jaracocontext.readthedocs.io/en/latest/history.html", + "jaraco.functools": "https://jaracofunctools.readthedocs.io/en/latest/history.html", + "backports.tarfile": "https://github.com/jaraco/backports.tarfile/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/check_coverage.py b/scripts/dev/check_coverage.py index 38a8f6ca1..e1d0d8642 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -73,8 +73,8 @@ PERFECT_FILES = [ 'qutebrowser/browser/history.py'), ('tests/unit/browser/test_pdfjs.py', 'qutebrowser/browser/pdfjs.py'), - ('tests/unit/browser/webkit/http/test_http.py', - 'qutebrowser/browser/webkit/http.py'), + ('tests/unit/browser/webkit/http/test_httpheaders.py', + 'qutebrowser/browser/webkit/httpheaders.py'), # ('tests/unit/browser/webkit/test_webkitelem.py', # 'qutebrowser/browser/webkit/webkitelem.py'), # ('tests/unit/browser/webkit/test_webkitelem.py', diff --git a/scripts/importer.py b/scripts/importer.py index 1914e6976..cf084d178 100755 --- a/scripts/importer.py +++ b/scripts/importer.py @@ -209,7 +209,7 @@ def import_html_bookmarks(bookmarks_file, bookmark_types, output_format): } bookmarks = [] for typ in bookmark_types: - tags = soup.findAll(bookmark_query[typ]) + tags = soup.find_all(bookmark_query[typ]) for tag in tags: if typ == 'search': tag['href'] = search_escape(tag['href']).replace('%s', '{}') diff --git a/tests/end2end/data/click_element.html b/tests/end2end/data/click_element.html index b2a691e08..7fac2a381 100644 --- a/tests/end2end/data/click_element.html +++ b/tests/end2end/data/click_element.html @@ -7,7 +7,7 @@ <span onclick='console.log("click_element special chars")'>"Don't", he shouted</span> <span>Duplicate</span> <span class='clickable' onclick='console.log("click_element CSS selector")'>Duplicate</span> - <form><input autofocus id='qute-input'></input></form> + <form><input id='qute-input' onfocus='console.log("qute-input focused")'></input></form> <a href="/data/hello.txt" id='link'>link</a> <span id='foo.bar' onclick='console.log("id with dot")'>ID with dot</span> <span style='position: absolute; left: 20px;top: 42px; width:10px; height:10px;' diff --git a/tests/end2end/data/insert_mode_settings/html/autofocus.html b/tests/end2end/data/insert_mode_settings/html/autofocus.html index 366f436f6..ca189b016 100644 --- a/tests/end2end/data/insert_mode_settings/html/autofocus.html +++ b/tests/end2end/data/insert_mode_settings/html/autofocus.html @@ -10,6 +10,9 @@ elem.addEventListener('input', function() { console.log("contents: " + elem.value); }); + elem.addEventListener('focus', function() { + console.log("autofocus element focused"); + }); } </script> </head> diff --git a/tests/end2end/features/caret.feature b/tests/end2end/features/caret.feature index 1302a1e6d..d6e65440c 100644 --- a/tests/end2end/features/caret.feature +++ b/tests/end2end/features/caret.feature @@ -34,11 +34,11 @@ Feature: Caret mode Scenario: :yank selection with --keep When I run :selection-toggle - And I run :move-to-end-of-word + And I run :move-to-next-word And I run :yank selection --keep And I run :move-to-end-of-word And I run :yank selection --keep - Then the message "3 chars yanked to clipboard" should be shown + Then the message "4 chars yanked to clipboard" should be shown And the message "7 chars yanked to clipboard" should be shown And the clipboard should contain "one two" diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 089f0c42c..082b999b1 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -684,8 +684,15 @@ def should_quit(qtbot, quteproc): def _get_scroll_values(quteproc): data = quteproc.get_session() - pos = data['windows'][0]['tabs'][0]['history'][-1]['scroll-pos'] - return (pos['x'], pos['y']) + + def get_active(things): + return next(thing for thing in things if thing.get("active")) + + active_window = get_active(data["windows"]) + active_tab = get_active(active_window["tabs"]) + current_entry = get_active(active_tab["history"]) + pos = current_entry["scroll-pos"] + return (pos["x"], pos["y"]) @bdd.then(bdd.parsers.re(r"the page should be scrolled " diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 1872ca5a6..90ce5334a 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -502,14 +502,13 @@ Feature: Various utility commands. Scenario: Clicking on focused element when there is none When I open data/click_element.html - # Need to loose focus on input element - And I run :click-element position 20,42 - And I wait for the javascript message "click_element position" And I run :click-element focused Then the error "No element found with focus!" should be shown Scenario: Clicking on focused element When I open data/click_element.html + And I run :fake-key <Tab> + And I wait for the javascript message "qute-input focused" And I run :click-element focused Then "Entering mode KeyMode.insert (reason: clicking input)" should be logged diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index 1b5306adb..ebacea890 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -3,6 +3,7 @@ Feature: Miscellaneous utility commands exposed to the user. Background: Given I open data/scroll/simple.html And I run :tab-only + And I run :window-only ## :cmd-later diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 98f7eff4d..b1e4bbaab 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -75,6 +75,8 @@ def is_ignored_lowlevel_message(message): 'glx: failed to create drisw screen', 'failed to load driver: zink', 'DRI3 not available', + # Webkit on arch with a newer mesa + 'MESA: error: ZINK: failed to load libvulkan.so.1', ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) @@ -213,6 +215,16 @@ def is_ignored_chromium_message(line): # [9895:9983:0904/043039.500565:ERROR:gpu_memory_buffer_support_x11.cc(49)] # dri3 extension not supported. "dri3 extension not supported.", + + # Qt 6.7 debug build + # [44513:44717:0325/173456.146759:WARNING:render_message_filter.cc(144)] + # Could not find tid + "Could not find tid", + + # [127693:127748:0325/230155.835421:WARNING:discardable_shared_memory_manager.cc(438)] + # Some MojoDiscardableSharedMemoryManagerImpls are still alive. They + # will be leaked. + "Some MojoDiscardableSharedMemoryManagerImpls are still alive. They will be leaked.", ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) @@ -395,9 +407,11 @@ class QuteProc(testprocess.Process): backend = 'webengine' if self.request.config.webengine else 'webkit' args = ['--debug', '--no-err-windows', '--temp-basedir', '--json-logging', '--loglevel', 'vdebug', - '--backend', backend, '--debug-flag', 'no-sql-history', - '--debug-flag', 'werror', '--debug-flag', - 'test-notification-service', + '--backend', backend, + '--debug-flag', 'no-sql-history', + '--debug-flag', 'werror', + '--debug-flag', 'test-notification-service', + '--debug-flag', 'caret', '--qt-flag', 'disable-features=PaintHoldingCrossOrigin'] if self.request.config.webengine and testutils.disable_seccomp_bpf_sandbox(): diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 5051efa85..924cb520b 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -16,6 +16,7 @@ import pytest from qutebrowser.qt.core import pyqtSignal, QUrl from end2end.fixtures import testprocess +from helpers import testutils class Request(testprocess.Line): @@ -111,6 +112,17 @@ class ExpectedRequest: return NotImplemented +def is_ignored_webserver_message(line: str) -> bool: + return testutils.pattern_match( + pattern=( + "Client ('127.0.0.1', *) lost — peer dropped the TLS connection suddenly, " + "during handshake: (1, '[SSL: SSLV3_ALERT_CERTIFICATE_UNKNOWN] ssl/tls " + "alert certificate unknown (_ssl.c:*)')" + ), + value=line, + ) + + class WebserverProcess(testprocess.Process): """Abstraction over a running Flask server process. @@ -151,7 +163,13 @@ class WebserverProcess(testprocess.Process): if started_re.fullmatch(line): self.ready.emit() return None - return Request(line) + + try: + return Request(line) + except testprocess.InvalidLine: + if is_ignored_webserver_message(line): + return None + raise def _executable_args(self): if hasattr(sys, 'frozen'): diff --git a/tests/end2end/test_insert_mode.py b/tests/end2end/test_insert_mode.py index abf32dbde..95757591d 100644 --- a/tests/end2end/test_insert_mode.py +++ b/tests/end2end/test_insert_mode.py @@ -43,7 +43,6 @@ def test_insert_mode(file_name, elem_id, source, input_text, zoom, (True, False, True), # enabled and foreground tab (True, True, False), # background tab ]) -@pytest.mark.flaky def test_auto_load(quteproc, auto_load, background, insert_mode): quteproc.set_setting('input.insert_mode.auto_load', str(auto_load)) url_path = 'data/insert_mode_settings/html/autofocus.html' diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index ba1da4145..d51cc69ff 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -9,7 +9,8 @@ import textwrap import pytest from qutebrowser.qt.core import QUrl -from qutebrowser.utils import usertypes +from qutebrowser.qt import machinery +from qutebrowser.utils import utils, usertypes from qutebrowser.browser import browsertab @@ -241,6 +242,13 @@ class TestWord: caret.move_to_end_of_word() selection.check("one") + @pytest.mark.xfail( + machinery.IS_QT6 and utils.is_windows, + reason=( + "move-to-end-of-word is broken with Qt 6 and Windows: " + "https://github.com/qutebrowser/qutebrowser/issues/8146" + ) + ) def test_moving_to_end_and_selecting_a_word(self, caret, selection): caret.move_to_end_of_word() selection.toggle() diff --git a/tests/unit/browser/webkit/http/test_content_disposition.py b/tests/unit/browser/webkit/http/test_content_disposition.py index 7cf80e3fd..4f3ef13c7 100644 --- a/tests/unit/browser/webkit/http/test_content_disposition.py +++ b/tests/unit/browser/webkit/http/test_content_disposition.py @@ -6,7 +6,7 @@ import logging import pytest -from qutebrowser.browser.webkit import http +from qutebrowser.browser.webkit import httpheaders DEFAULT_NAME = 'qutebrowser-download' @@ -30,7 +30,7 @@ class HeaderChecker: """Check if the passed header has the given filename.""" reply = self.stubs.FakeNetworkReply( headers={'Content-Disposition': header}) - cd_inline, cd_filename = http.parse_content_disposition(reply) + cd_inline, cd_filename = httpheaders.parse_content_disposition(reply) assert cd_filename is not None assert cd_filename == filename assert cd_inline == expected_inline @@ -40,7 +40,7 @@ class HeaderChecker: reply = self.stubs.FakeNetworkReply( headers={'Content-Disposition': header}) with self.caplog.at_level(logging.ERROR, 'network'): - cd_inline, cd_filename = http.parse_content_disposition(reply) + cd_inline, cd_filename = httpheaders.parse_content_disposition(reply) assert cd_filename == DEFAULT_NAME assert cd_inline @@ -48,7 +48,7 @@ class HeaderChecker: """Check if the passed header results in an unnamed attachment.""" reply = self.stubs.FakeNetworkReply( headers={'Content-Disposition': header}) - cd_inline, cd_filename = http.parse_content_disposition(reply) + cd_inline, cd_filename = httpheaders.parse_content_disposition(reply) assert cd_filename == DEFAULT_NAME assert not cd_inline @@ -164,7 +164,7 @@ class TestAttachment: """ reply = stubs.FakeNetworkReply( headers={'Content-Disposition': 'attachment'}) - cd_inline, cd_filename = http.parse_content_disposition(reply) + cd_inline, cd_filename = httpheaders.parse_content_disposition(reply) assert not cd_inline assert cd_filename == DEFAULT_NAME diff --git a/tests/unit/browser/webkit/http/test_http.py b/tests/unit/browser/webkit/http/test_httpheaders.py index 210d79486..7368575e8 100644 --- a/tests/unit/browser/webkit/http/test_http.py +++ b/tests/unit/browser/webkit/http/test_httpheaders.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -"""Tests for qutebrowser.browser.webkit.http.""" +"""Tests for qutebrowser.browser.webkit.httpheaders.""" import logging @@ -11,7 +11,7 @@ import hypothesis from hypothesis import strategies from qutebrowser.qt.core import QUrl -from qutebrowser.browser.webkit import http +from qutebrowser.browser.webkit import httpheaders @pytest.mark.parametrize('url, expected', [ @@ -24,7 +24,7 @@ from qutebrowser.browser.webkit import http ]) def test_no_content_disposition(stubs, url, expected): reply = stubs.FakeNetworkReply(url=QUrl(url)) - inline, filename = http.parse_content_disposition(reply) + inline, filename = httpheaders.parse_content_disposition(reply) assert inline assert filename == expected @@ -40,8 +40,8 @@ def test_no_content_disposition(stubs, url, expected): # dropping QtWebKit. ]) def test_parse_content_disposition_invalid(value): - with pytest.raises(http.ContentDispositionError): - http.ContentDisposition.parse(value) + with pytest.raises(httpheaders.ContentDispositionError): + httpheaders.ContentDisposition.parse(value) @pytest.mark.parametrize('template', [ @@ -58,16 +58,16 @@ def test_parse_content_disposition_hypothesis(caplog, template, stubs, s): header = template.format(s) reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header}) with caplog.at_level(logging.ERROR, 'network'): - http.parse_content_disposition(reply) + httpheaders.parse_content_disposition(reply) @hypothesis.given(strategies.binary()) def test_content_disposition_directly_hypothesis(s): """Test rfc6266 parsing directly with binary data.""" try: - cd = http.ContentDisposition.parse(s) + cd = httpheaders.ContentDisposition.parse(s) cd.filename() - except http.ContentDispositionError: + except httpheaders.ContentDispositionError: pass @@ -83,7 +83,7 @@ def test_parse_content_type(stubs, content_type, expected_mimetype, reply = stubs.FakeNetworkReply() else: reply = stubs.FakeNetworkReply(headers={'Content-Type': content_type}) - mimetype, rest = http.parse_content_type(reply) + mimetype, rest = httpheaders.parse_content_type(reply) assert mimetype == expected_mimetype assert rest == expected_rest @@ -91,4 +91,4 @@ def test_parse_content_type(stubs, content_type, expected_mimetype, @hypothesis.given(strategies.text()) def test_parse_content_type_hypothesis(stubs, s): reply = stubs.FakeNetworkReply(headers={'Content-Type': s}) - http.parse_content_type(reply) + httpheaders.parse_content_type(reply) diff --git a/tests/unit/browser/webkit/test_certificateerror.py b/tests/unit/browser/webkit/test_certificateerror.py index 2feb7dcf1..7f5cce9c6 100644 --- a/tests/unit/browser/webkit/test_certificateerror.py +++ b/tests/unit/browser/webkit/test_certificateerror.py @@ -18,15 +18,15 @@ class FakeError: return self.msg -@pytest.mark.parametrize('errors, expected', [ +@pytest.mark.parametrize('error_factories, expected', [ ( - [QSslError(QSslError.SslError.UnableToGetIssuerCertificate)], + [lambda: QSslError(QSslError.SslError.UnableToGetIssuerCertificate)], ['<p>The issuer certificate could not be found</p>'], ), ( [ - QSslError(QSslError.SslError.UnableToGetIssuerCertificate), - QSslError(QSslError.SslError.UnableToDecryptCertificateSignature), + lambda: QSslError(QSslError.SslError.UnableToGetIssuerCertificate), + lambda: QSslError(QSslError.SslError.UnableToDecryptCertificateSignature), ], [ '<ul>', @@ -37,13 +37,13 @@ class FakeError: ), ( - [FakeError('Escaping test: <>')], + [lambda: FakeError('Escaping test: <>')], ['<p>Escaping test: <></p>'], ), ( [ - FakeError('Escaping test 1: <>'), - FakeError('Escaping test 2: <>'), + lambda: FakeError('Escaping test 1: <>'), + lambda: FakeError('Escaping test 2: <>'), ], [ '<ul>', @@ -53,8 +53,9 @@ class FakeError: ], ), ]) -def test_html(stubs, errors, expected): +def test_html(stubs, error_factories, expected): reply = stubs.FakeNetworkReply(url=QUrl("https://example.com")) + errors = [factory() for factory in error_factories] wrapper = certificateerror.CertificateErrorWrapper(reply=reply, errors=errors) lines = [line.strip() for line in wrapper.html().splitlines() if line.strip()] assert lines == expected diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index d908e3ac6..bcd257ed7 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1874,24 +1874,24 @@ class TestProxy: def klass(self): return configtypes.Proxy - @pytest.mark.parametrize('val, expected', [ - ('system', configtypes.SYSTEM_PROXY), - ('none', QNetworkProxy(QNetworkProxy.ProxyType.NoProxy)), + @pytest.mark.parametrize('val, expected_factory', [ + ('system', lambda: configtypes.SYSTEM_PROXY), + ('none', lambda: QNetworkProxy(QNetworkProxy.ProxyType.NoProxy)), ('socks://example.com/', - QNetworkProxy(QNetworkProxy.ProxyType.Socks5Proxy, 'example.com')), + lambda: QNetworkProxy(QNetworkProxy.ProxyType.Socks5Proxy, 'example.com')), ('socks5://foo:bar@example.com:2323', - QNetworkProxy(QNetworkProxy.ProxyType.Socks5Proxy, 'example.com', 2323, - 'foo', 'bar')), + lambda: QNetworkProxy( + QNetworkProxy.ProxyType.Socks5Proxy, 'example.com', 2323, 'foo', 'bar')), ('pac+http://example.com/proxy.pac', - pac.PACFetcher(QUrl('pac+http://example.com/proxy.pac'))), + lambda: pac.PACFetcher(QUrl('pac+http://example.com/proxy.pac'))), ('pac+file:///tmp/proxy.pac', - pac.PACFetcher(QUrl('pac+file:///tmp/proxy.pac'))), + lambda: pac.PACFetcher(QUrl('pac+file:///tmp/proxy.pac'))), ]) - def test_to_py_valid(self, klass, val, expected): + def test_to_py_valid(self, klass, val, expected_factory): actual = klass().to_py(val) if isinstance(actual, QNetworkProxy): actual = QNetworkProxy(actual) - assert actual == expected + assert actual == expected_factory() @pytest.mark.parametrize('val', [ 'blah', diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 3aceca668..c7af3162c 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -208,6 +208,18 @@ def test_ensure_valid(obj, raising, exc_reason, exc_str): "The data stream has read corrupt data."), (QDataStream.Status.WriteFailed, True, "The data stream cannot write to the underlying device."), + pytest.param( + getattr(QDataStream.Status, "SizeLimitExceeded", None), + True, + ( + "The data stream cannot read or write the data because its size is larger " + "than supported by the current platform." + ), + marks=pytest.mark.skipif( + not hasattr(QDataStream.Status, "SizeLimitExceeded"), + reason="Added in Qt 6.7" + ) + ), ]) def test_check_qdatastream(status, raising, message): """Test check_qdatastream. @@ -226,10 +238,25 @@ def test_check_qdatastream(status, raising, message): qtutils.check_qdatastream(stream) -def test_qdatastream_status_count(): - """Make sure no new members are added to QDataStream.Status.""" - status_vals = testutils.enum_members(QDataStream, QDataStream.Status) - assert len(status_vals) == 4 +def test_qdatastream_status_members(): + """Make sure no new members are added to QDataStream.Status. + + If this fails, qtutils.check_qdatastream will need to be updated with the + respective error documentation. + """ + status_vals = set(testutils.enum_members(QDataStream, QDataStream.Status).values()) + expected = { + QDataStream.Status.Ok, + QDataStream.Status.ReadPastEnd, + QDataStream.Status.ReadCorruptData, + QDataStream.Status.WriteFailed, + } + try: + expected.add(QDataStream.Status.SizeLimitExceeded) + except AttributeError: + # Added in Qt 6.7 + pass + assert status_vals == expected @pytest.mark.parametrize('color, expected', [ @@ -69,8 +69,8 @@ setenv = pip_pre = true deps = -r{toxinidir}/misc/requirements/requirements-tests-bleeding.txt commands_pre = - qt5: pip install --extra-index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt5 PyQtWebEngine PyQt5-Qt5 PyQtWebEngine-Qt5 PyQt5-sip - !qt5: pip install --extra-index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt6 PyQt6-WebEngine PyQt6-Qt6 PyQt6-WebEngine-Qt6 PyQt6-sip + qt5: pip install --extra-index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade --only-binary PyQt5,PyQtWebEngine PyQt5 PyQtWebEngine PyQt5-Qt5 PyQtWebEngine-Qt5 PyQt5-sip + !qt5: pip install --extra-index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade --only-binary PyQt6,PyQt6-WebEngine PyQt6 PyQt6-WebEngine PyQt6-Qt6 PyQt6-WebEngine-Qt6 PyQt6-sip commands = {envpython} -bb -m pytest {posargs:tests} # other envs |