diff options
42 files changed, 392 insertions, 158 deletions
diff --git a/.github/workflows/bleeding.yml b/.github/workflows/bleeding.yml index 59da1dfad..8f8cddc1e 100644 --- a/.github/workflows/bleeding.yml +++ b/.github/workflows/bleeding.yml @@ -12,12 +12,20 @@ jobs: if: "github.repository == 'qutebrowser/qutebrowser'" runs-on: ubuntu-20.04 timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - testenv: bleeding + image: "archlinux-webengine-unstable-qt6" + - testenv: bleeding-qt5 + image: "archlinux-webengine-unstable" container: - image: "qutebrowser/ci:archlinux-webengine-unstable" + image: "qutebrowser/ci:${{ matrix.image }}" env: FORCE_COLOR: "1" PY_COLORS: "1" - DOCKER: "archlinux-webengine-unstable" + DOCKER: "${{ matrix.image }}" CI: true volumes: # Hardcoded because we can't use ${{ runner.temp }} here apparently. @@ -30,7 +38,7 @@ jobs: - name: Set up problem matchers run: "python scripts/dev/ci/problemmatchers.py py3 ${{ runner.temp }}" - name: Run tox - run: dbus-run-session tox -e bleeding + run: dbus-run-session tox -e ${{ matrix.testenv }} irc: timeout-minutes: 2 continue-on-error: true diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index b2b392a4c..96a4b42e7 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -67,6 +67,8 @@ Added - New `colors.webpage.darkmode.increase_text_contrast` setting for Qt 6.3+ - New `fonts.tooltip`, `colors.tooltip.bg` and `colors.tooltip.fg` settings. - New `log-qt-events` debug flag for `-D` +- New `--all` flags for `:bookmark-del` and `:quickmark-del` to delete all + quickmarks/bookmarks. Removed ~~~~~~~ @@ -86,7 +88,7 @@ Removed * Qt 6.4 was the latest version to support macOS 10.14 and 10.15. * It should be possible to build a custom .dmg with Qt 6.4, but this is unsupported and not recommended. -- Support for Windows 8 and for Windows 10 before 1809 is now dropped. +- Support for Windows 8 and for Windows 10 before 1607 is now dropped. * Support for older Windows 10 versions might still be present in Qt 6.0/6.1/6.2 * Support for Windows 8.1 is still present in Qt 5.15 * It should be possible to build a custom .exe with those versions, but this diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index d35d01079..4d1610970 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -204,7 +204,7 @@ If no url and title are provided, then save the current page as a bookmark. If a [[bookmark-del]] === bookmark-del -Syntax: +:bookmark-del ['url']+ +Syntax: +:bookmark-del [*--all*] ['url']+ Delete a bookmark. @@ -212,6 +212,9 @@ Delete a bookmark. * +'url'+: The url of the bookmark to delete. If not given, use the current page's url. +==== optional arguments +* +*-a*+, +*--all*+: If given, delete all bookmarks. + ==== note * This command does not split arguments after the last argument and handles quotes literally. @@ -1064,7 +1067,7 @@ You can view all saved quickmarks on the link:qute://bookmarks[bookmarks page]. [[quickmark-del]] === quickmark-del -Syntax: +:quickmark-del ['name']+ +Syntax: +:quickmark-del [*--all*] ['name']+ Delete a quickmark. @@ -1073,6 +1076,9 @@ Delete a quickmark. if there are more than one). +==== optional arguments +* +*-a*+, +*--all*+: Delete all quickmarks. + ==== note * This command does not split arguments after the last argument and handles quotes literally. diff --git a/misc/nsis/install.nsh b/misc/nsis/install.nsh index e7d8b4956..282a254eb 100755 --- a/misc/nsis/install.nsh +++ b/misc/nsis/install.nsh @@ -430,8 +430,37 @@ SectionEnd ; Callbacks Function .onInit StrCpy $KeepReg 1 - !insertmacro CheckPlatform ${PLATFORM} - !insertmacro CheckMinWinVer ${MIN_WIN_VER} + +; OS version check + ${If} ${RunningX64} + ; https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa#remarks + GetWinVer $R0 Major + !if "${QT5}" == "True" + IntCmpU $R0 6 0 _os_check_fail _os_check_pass + GetWinVer $R1 Minor + IntCmpU $R1 2 _os_check_pass _os_check_fail _os_check_pass + !else + IntCmpU $R0 10 0 _os_check_fail _os_check_pass + GetWinVer $R1 Build + ${If} $R1 >= 22000 ; Windows 11 21H2 + Goto _os_check_pass + ${ElseIf} $R1 >= 14393 ; Windows 10 1607 + ${AndIf} ${IsNativeAMD64} ; Windows 10 has no x86_64 emulation on arm64 + Goto _os_check_pass + ${EndIf} + !endif + ${EndIf} + _os_check_fail: + !if "${QT5}" == "True" + MessageBox MB_OK|MB_ICONSTOP "This version of ${PRODUCT_NAME} requires a 64-bit$\r$\n\ + version of Windows 8 or later." + !else + MessageBox MB_OK|MB_ICONSTOP "This version of ${PRODUCT_NAME} requires a 64-bit$\r$\n\ + version of Windows 10 1607 or later." + !endif + Abort + _os_check_pass: + ${ifnot} ${UAC_IsInnerInstance} !insertmacro CheckSingleInstance "Setup" "Global" "${SETUP_MUTEX}" !insertmacro CheckSingleInstance "Application" "Local" "${APP_MUTEX}" diff --git a/misc/nsis/qutebrowser.nsi b/misc/nsis/qutebrowser.nsi index 60b174bdd..bd5156e83 100755 --- a/misc/nsis/qutebrowser.nsi +++ b/misc/nsis/qutebrowser.nsi @@ -124,13 +124,16 @@ ShowUninstDetails hide ; If not defined, get VERSION from PROGEXE. Set DIST_DIR accordingly.
!ifndef VERSION
- !define /ifndef DIST_DIR ".\..\..\dist\${PRODUCT_NAME}-${ARCH}"
+ !define /ifndef DIST_DIR ".\..\..\dist\${PRODUCT_NAME}"
!getdllversion "${DIST_DIR}\${PROGEXE}" expv_
!define VERSION "${expv_1}.${expv_2}.${expv_3}"
!else
- !define /ifndef DIST_DIR ".\..\..\dist\${PRODUCT_NAME}-${VERSION}-${ARCH}"
+ !define /ifndef DIST_DIR ".\..\..\dist\${PRODUCT_NAME}-${VERSION}"
!endif
+; If not defined, assume Qt6 (requires a more recent windows version)
+!define /ifndef QT5 "False"
+
; Pack the exe header with upx if UPX is defined.
!ifdef UPX
!packhdr "$%TEMP%\exehead.tmp" '"upx" "--ultra-brute" "$%TEMP%\exehead.tmp"'
diff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec index 1eee9161d..ecb9da68e 100644 --- a/misc/qutebrowser.spec +++ b/misc/qutebrowser.spec @@ -64,17 +64,17 @@ INFO_PLIST_UPDATES = { def get_data_files(): data_files = [ - ('../qutebrowser/html', 'html'), - ('../qutebrowser/img', 'img'), - ('../qutebrowser/icons', 'icons'), - ('../qutebrowser/javascript', 'javascript'), - ('../qutebrowser/html/doc', 'html/doc'), - ('../qutebrowser/git-commit-id', '.'), - ('../qutebrowser/config/configdata.yml', 'config'), + ('../qutebrowser/html', 'qutebrowser/html'), + ('../qutebrowser/img', 'qutebrowser/img'), + ('../qutebrowser/icons', 'qutebrowser/icons'), + ('../qutebrowser/javascript', 'qutebrowser/javascript'), + ('../qutebrowser/html/doc', 'qutebrowser/html/doc'), + ('../qutebrowser/git-commit-id', 'qutebrowser/git-commit-id'), + ('../qutebrowser/config/configdata.yml', 'qutebrowser/config'), ] if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')): - data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs')) + data_files.append(('../qutebrowser/3rdparty/pdfjs', 'qutebrowser/3rdparty/pdfjs')) else: print("Warning: excluding pdfjs as it's not present!") @@ -137,5 +137,4 @@ app = BUNDLE(coll, name='qutebrowser.app', icon=icon, info_plist=INFO_PLIST_UPDATES, - # https://github.com/pyinstaller/pyinstaller/blob/b78bfe530cdc2904f65ce098bdf2de08c9037abb/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py#L24 - bundle_identifier='org.qt-project.Qt.QtWebEngineCore') + bundle_identifier='org.qutebrowser.qutebrowser') diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index c9590f1c5..61a10c38e 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -12,7 +12,7 @@ github3.py==4.0.1 hunter==3.6.1 idna==3.4 importlib-metadata==6.8.0 -importlib-resources==6.0.0 +importlib-resources==6.0.1 jaraco.classes==3.3.0 jeepney==0.8.0 keyring==24.2.0 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 1e18a7ab2..1faf875fd 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -2,18 +2,18 @@ chardet==5.2.0 diff-cover==7.7.0 -importlib-resources==6.0.0 +importlib-resources==6.0.1 Jinja2==3.1.2 lxml==4.9.3 MarkupSafe==2.1.3 -mypy==1.4.1 +mypy==1.5.0 mypy-extensions==1.0.0 pluggy==1.2.0 Pygments==2.16.1 PyQt5-stubs==5.15.6.0 tomli==2.0.1 types-colorama==0.4.15.12 -types-docutils==0.20.0.1 +types-docutils==0.20.0.2 types-Pygments==2.16.0.0 types-PyYAML==6.0.12.11 types-setuptools==68.0.0.3 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 912b38cd3..b112963b0 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py altgraph==0.17.3 -pyinstaller==5.13.0 +pyinstaller @ git+https://github.com/pyinstaller/pyinstaller.git@79f62ef29822169ae00cd4271390d0e3175476ad pyinstaller-hooks-contrib==2023.6 diff --git a/misc/requirements/requirements-pyinstaller.txt-raw b/misc/requirements/requirements-pyinstaller.txt-raw index c313980b0..7b4c8c84c 100644 --- a/misc/requirements/requirements-pyinstaller.txt-raw +++ b/misc/requirements/requirements-pyinstaller.txt-raw @@ -1 +1 @@ -PyInstaller +pyinstaller @ git+https://github.com/pyinstaller/pyinstaller.git@79f62ef29822169ae00cd4271390d0e3175476ad diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index f574b4c26..0bed26a8d 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -11,5 +11,5 @@ pyproject_hooks==1.0.0 pyroma==4.2 requests==2.31.0 tomli==2.0.1 -trove-classifiers==2023.7.6 +trove-classifiers==2023.8.7 urllib3==2.0.4 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index abd6ea727..a51a099c7 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -7,13 +7,13 @@ certifi==2023.7.22 charset-normalizer==3.2.0 cheroot==10.0.0 click==8.1.6 -coverage==7.2.7 +coverage==7.3.0 exceptiongroup==1.1.2 execnet==2.0.2 filelock==3.12.2 Flask==2.3.2 hunter==3.6.1 -hypothesis==6.82.2 +hypothesis==6.82.4 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 @@ -52,6 +52,6 @@ toml==0.10.2 tomli==2.0.1 typing_extensions==4.7.1 urllib3==2.0.4 -vulture==2.7 +vulture==2.8 Werkzeug==2.3.6 zipp==3.16.2 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index ae8fce6ff..064536480 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -12,6 +12,6 @@ pluggy==1.2.0 pyproject-api==1.5.3 setuptools==68.0.0 tomli==2.0.1 -tox==4.6.4 -virtualenv==20.24.2 +tox==4.8.0 +virtualenv==20.24.3 wheel==0.41.1 diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt index f72d24a67..8c98a265e 100644 --- a/misc/requirements/requirements-vulture.txt +++ b/misc/requirements/requirements-vulture.txt @@ -1,4 +1,4 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py toml==0.10.2 -vulture==2.7 +vulture==2.8 diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index 0c367c6bf..e5466f072 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -101,7 +101,7 @@ class _CmdHandlerType(Protocol): Below, we cast the decorated function to _CmdHandlerType to make mypy aware of this. """ - qute_args: Optional[Dict[str, command.ArgInfo]] + qute_args: Optional[Dict[str, 'command.ArgInfo']] def __call__(self, *args: Any, **kwargs: Any) -> Any: ... diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 94cc53c72..778c248c2 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -346,15 +346,6 @@ def _open_special_pages(args): True, 'qute://warning/sessions'), - ('sandboxing-warning-shown', - ( - hasattr(sys, "frozen") and - utils.is_mac and - machinery.IS_QT6 and - os.environ.get("QTWEBENGINE_DISABLE_SANDBOX") == "1" - ), - 'qute://warning/sandboxing'), - ('qt5-warning-shown', ( machinery.IS_QT5 and @@ -570,7 +561,7 @@ class Application(QApplication): @pyqtSlot(QObject) def on_focus_object_changed(self, obj): """Log when the focus object changed.""" - output = repr(obj) + output = qtutils.qobj_repr(obj) if self._last_focus_object != output: log.misc.debug("Focus object changed: {}".format(output)) self._last_focus_object = output diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index b6cc303cf..495d4325d 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: from qutebrowser.keyinput import modeman from qutebrowser.config import config, websettings from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, - urlutils, message, jinja) + urlutils, message, jinja, version) from qutebrowser.misc import miscwidgets, objects, sessions from qutebrowser.browser import eventfilter, inspector from qutebrowser.qt import sip @@ -1169,6 +1169,23 @@ class AbstractTab(QWidget): navigation.url.errorString())) navigation.accepted = False + # WORKAROUND for QtWebEngine >= 6.2 not allowing form requests from + # qute:// to outside domains. + if ( + self.url() == QUrl("qute://start/") and + navigation.navigation_type == navigation.Type.form_submitted and + navigation.url.matches( + QUrl(config.val.url.searchengines['DEFAULT']), + urlutils.FormatOption.REMOVE_QUERY) and + objects.backend == usertypes.Backend.QtWebEngine and + version.qtwebengine_versions().webengine >= utils.VersionNumber(6, 2) + ): + log.webview.debug( + "Working around qute://start loading issue for " + f"{navigation.url.toDisplayString()}") + navigation.accepted = False + self.load_url(navigation.url) + @pyqtSlot(bool) def _on_load_finished(self, ok: bool) -> None: assert self._widget is not None diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 7efb69511..83a846b85 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1235,21 +1235,31 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) @cmdutils.argument('name', completion=miscmodels.quickmark) - def quickmark_del(self, name=None): + def quickmark_del(self, name=None, all_=False): """Delete a quickmark. Args: name: The name of the quickmark to delete. If not given, delete the quickmark for the current page (choosing one arbitrarily if there are more than one). + all_: Delete all quickmarks. """ quickmark_manager = objreg.get('quickmark-manager') + + if all_: + if name is not None: + raise cmdutils.CommandError("Cannot specify name and --all") + quickmark_manager.clear() + message.info("Quickmarks cleared.") + return + if name is None: url = self._current_url() try: name = quickmark_manager.get_by_qurl(url) except urlmarks.DoesNotExistError as e: raise cmdutils.CommandError(str(e)) + try: quickmark_manager.delete(name) except KeyError: @@ -1320,18 +1330,28 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) @cmdutils.argument('url', completion=miscmodels.bookmark) - def bookmark_del(self, url=None): + def bookmark_del(self, url=None, all_=False): """Delete a bookmark. Args: url: The url of the bookmark to delete. If not given, use the current page's url. + all_: If given, delete all bookmarks. """ + bookmark_manager = objreg.get('bookmark-manager') + if all_: + if url is not None: + raise cmdutils.CommandError("Cannot specify url and --all") + bookmark_manager.clear() + message.info("Bookmarks cleared.") + return + if url is None: url = self._current_url().toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded) + try: - objreg.get('bookmark-manager').delete(url) + bookmark_manager.delete(url) except KeyError: raise cmdutils.CommandError("Bookmark '{}' not found!".format(url)) message.info("Removed bookmark {}".format(url)) diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py index 8dbfbd008..6404608b3 100644 --- a/qutebrowser/browser/eventfilter.py +++ b/qutebrowser/browser/eventfilter.py @@ -6,10 +6,12 @@ from qutebrowser.qt import machinery from qutebrowser.qt.core import QObject, QEvent, Qt, QTimer +from qutebrowser.qt.widgets import QWidget from qutebrowser.config import config -from qutebrowser.utils import log, message, usertypes +from qutebrowser.utils import log, message, usertypes, qtutils, version, utils from qutebrowser.keyinput import modeman +from qutebrowser.misc import objects class ChildEventFilter(QObject): @@ -35,17 +37,42 @@ class ChildEventFilter(QObject): """Act on ChildAdded events.""" if event.type() == QEvent.Type.ChildAdded: child = event.child() - log.misc.debug("{} got new child {}, installing filter" - .format(obj, child)) + log.misc.debug( + f"{qtutils.qobj_repr(obj)} got new child {qtutils.qobj_repr(child)}, " + "installing filter") # Additional sanity check, but optional if self._widget is not None: assert obj is self._widget + # WORKAROUND for unknown Qt bug losing focus on child change + # Carry on keyboard focus to the new child if: + # - This is a child event filter on a tab (self._widget is not None) + # - We find an old existing child which is a QQuickWidget and is + # currently focused. + # - We're using QtWebEngine >= 6.4 (older versions are not affected) + children = [ + c for c in self._widget.findChildren( + QWidget, "", Qt.FindChildOption.FindDirectChildrenOnly) + if c is not child and + c.hasFocus() and + c.metaObject() is not None and + c.metaObject().className() == "QQuickWidget" + ] + if ( + children and + objects.backend == usertypes.Backend.QtWebEngine and + version.qtwebengine_versions().webengine >= + utils.VersionNumber(6, 4) + ): + log.misc.debug("Focusing new child") + child.setFocus() + child.installEventFilter(self._filter) elif event.type() == QEvent.Type.ChildRemoved: child = event.child() - log.misc.debug("{}: removed child {}".format(obj, child)) + log.misc.debug( + f"{qtutils.qobj_repr(obj)}: removed child {qtutils.qobj_repr(child)}") return False diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index dae862b8b..f325ff9e3 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -568,9 +568,6 @@ def qute_warning(url: QUrl) -> _HandlerRet: title='Qt 5.15 sessions warning', datadir=standarddir.data(), sep=os.sep) - 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', diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 2a060f5ef..2d2563a1a 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -98,6 +98,11 @@ class UrlMarkManager(QObject): del self.marks[key] self.changed.emit() + def clear(self): + """Delete all marks.""" + self.marks.clear() + self.changed.emit() + class QuickmarkManager(UrlMarkManager): diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index d383c1aa7..20c3a36d4 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -11,7 +11,7 @@ from qutebrowser.qt.core import QRect, QEventLoop from qutebrowser.qt.widgets import QApplication from qutebrowser.qt.webenginecore import QWebEngineSettings -from qutebrowser.utils import log, javascript, urlutils, usertypes, utils +from qutebrowser.utils import log, javascript, urlutils, usertypes, utils, version from qutebrowser.browser import webelem if TYPE_CHECKING: @@ -213,6 +213,17 @@ class WebEngineElement(webelem.AbstractWebElement): return True if baseurl.scheme() == url.scheme(): # e.g. a qute:// link return False + + # Qt 6.3+ needs a user interaction to allow navigations from qute:// to + # outside qute:// (like e.g. on qute://bookmarks). + versions = version.qtwebengine_versions() + if ( + baseurl.scheme() == "qute" and + url.scheme() != "qute" and + versions.webengine >= utils.VersionNumber(6, 3) + ): + return True + return url.scheme() not in urlutils.WEBENGINE_SCHEMES def _click_editable(self, click_target: usertypes.ClickTarget) -> None: diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index b5b232c5a..7ccdabc88 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -6,11 +6,12 @@ import pkgutil import types +import sys import pathlib import importlib import argparse import dataclasses -from typing import Callable, Iterator, List, Optional, Tuple +from typing import Callable, Iterator, List, Optional, Set, Tuple from qutebrowser.qt.core import pyqtSlot @@ -79,6 +80,14 @@ def load_components(*, skip_hooks: bool = False) -> None: def walk_components() -> Iterator[ExtensionInfo]: """Yield ExtensionInfo objects for all modules.""" + if hasattr(sys, 'frozen'): + yield from _walk_pyinstaller() + else: + yield from _walk_normal() + + +def _walk_normal() -> Iterator[ExtensionInfo]: + """Walk extensions when not using PyInstaller.""" for _finder, name, ispkg in pkgutil.walk_packages( path=components.__path__, prefix=components.__name__ + '.', @@ -93,6 +102,23 @@ def walk_components() -> Iterator[ExtensionInfo]: yield ExtensionInfo(name=name) +def _walk_pyinstaller() -> Iterator[ExtensionInfo]: + """Walk extensions when using PyInstaller. + + See https://github.com/pyinstaller/pyinstaller/issues/1905 + + Inspired by: + https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py + """ + toc: Set[str] = set() + for importer in pkgutil.iter_importers('qutebrowser'): + if hasattr(importer, 'toc'): + toc |= importer.toc + for name in toc: + if name.startswith(components.__name__ + '.'): + yield ExtensionInfo(name=name) + + def _get_init_context() -> InitContext: """Get an InitContext object.""" return InitContext(data_dir=pathlib.Path(standarddir.data()), diff --git a/qutebrowser/html/warning-sandboxing.html b/qutebrowser/html/warning-sandboxing.html deleted file mode 100644 index 186d938e7..000000000 --- a/qutebrowser/html/warning-sandboxing.html +++ /dev/null @@ -1,16 +0,0 @@ -{% 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/sandboxing</span> to show it again at a later time.</span> - -<p> - Due to a <a href="https://github.com/pyinstaller/pyinstaller/pull/6903">PyInstaller issue</a>, - Chromium's <a href="https://chromium.googlesource.com/chromium/src/+/HEAD/docs/design/sandbox_faq.md">sandboxing</a> - is currently disabled for macOS builds with Qt 6. This means that there will be no additional layer of protection - in case of Chromium security bugs. Thus, it's advised to - <b>not use this build in production</b>. Hopefully, this situation will be - resolved before the final 3.0.0 release. -</p> -{% endblock %} diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 582a1bf18..f0337ec88 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -16,7 +16,7 @@ from qutebrowser.commands import runners from qutebrowser.keyinput import modeparsers, basekeyparser from qutebrowser.config import config from qutebrowser.api import cmdutils -from qutebrowser.utils import usertypes, log, objreg, utils +from qutebrowser.utils import usertypes, log, objreg, utils, qtutils from qutebrowser.browser import hints from qutebrowser.misc import objects @@ -308,10 +308,10 @@ class ModeManager(QObject): focus_widget = objects.qapp.focusWidget() log.modes.debug("match: {}, forward_unbound_keys: {}, " "passthrough: {}, is_non_alnum: {}, dry_run: {} " - "--> filter: {} (focused: {!r})".format( + "--> filter: {} (focused: {})".format( match, forward_unbound_keys, parser.passthrough, is_non_alnum, dry_run, - filter_this, focus_widget)) + filter_this, qtutils.qobj_repr(focus_widget))) return filter_this def _handle_keyrelease(self, event: QKeyEvent) -> bool: diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index a576b34ff..fdb085a1f 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -254,8 +254,10 @@ class RegisterKeyParser(CommandKeyParser): mode: usertypes.KeyMode, commandrunner: 'runners.CommandRunner', parent: QObject = None) -> None: - super().__init__(mode=usertypes.KeyMode.register, win_id=win_id, - commandrunner=commandrunner, parent=parent, + super().__init__(mode=usertypes.KeyMode.register, # type: ignore[arg-type] + win_id=win_id, + commandrunner=commandrunner, + parent=parent, supports_count=False) self._register_mode = mode diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 2e90c46c4..c0c7ee2ad 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -396,6 +396,7 @@ class TabBar(QTabBar): self._win_id = win_id self._our_style = TabBarStyle() self.setStyle(self._our_style) + self.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.vertical = False self._auto_hide_timer = QTimer() self._auto_hide_timer.setSingleShot(True) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 5e7c6d272..ebcd6578f 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -639,6 +639,38 @@ def extract_enum_val(val: Union[sip.simplewrapper, int, enum.Enum]) -> int: return val +def qobj_repr(obj: Optional[QObject]) -> str: + """Show nicer debug information for a QObject.""" + py_repr = repr(obj) + if obj is None: + return py_repr + + try: + object_name = obj.objectName() + meta_object = obj.metaObject() + except AttributeError: + # Technically not possible if obj is a QObject, but crashing when trying to get + # some debug info isn't helpful. + return py_repr + + class_name = "" if meta_object is None else meta_object.className() + + if py_repr.startswith("<") and py_repr.endswith(">"): + # With a repr such as <QObject object at 0x...>, we want to end up with: + # <QObject object at 0x..., objectName='...'> + # But if we have RichRepr() as existing repr, we want: + # <RichRepr(), objectName='...'> + py_repr = py_repr[1:-1] + + parts = [py_repr] + if object_name: + parts.append(f"objectName={object_name!r}") + if class_name and f".{class_name} object at 0x" not in py_repr: + parts.append(f"className={class_name!r}") + + return f"<{', '.join(parts)}>" + + _T = TypeVar("_T") diff --git a/qutebrowser/utils/resources.py b/qutebrowser/utils/resources.py index 494f01bff..60d90fd31 100644 --- a/qutebrowser/utils/resources.py +++ b/qutebrowser/utils/resources.py @@ -36,11 +36,6 @@ def _path(filename: str) -> _ResourceType: assert not posixpath.isabs(filename), filename assert os.path.pardir not in filename.split(posixpath.sep), filename - if hasattr(sys, 'frozen'): - # For PyInstaller, where we can't store resource files in a qutebrowser/ folder - # because the executable is already named "qutebrowser" (at least on macOS). - return pathlib.Path(sys.executable).parent / filename - return importlib_resources.files(qutebrowser) / filename @contextlib.contextmanager diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index c347ae53b..0b571946d 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -39,6 +39,7 @@ if machinery.IS_QT6: REMOVE_SCHEME = QUrl.UrlFormattingOption.RemoveScheme REMOVE_PASSWORD = QUrl.UrlFormattingOption.RemovePassword + REMOVE_QUERY = QUrl.UrlFormattingOption.RemoveQuery else: UrlFlagsType = Union[ QUrl.FormattingOptions, @@ -74,6 +75,8 @@ else: _QtFormattingOptions, QUrl.UrlFormattingOption.RemoveScheme) REMOVE_PASSWORD = cast( _QtFormattingOptions, QUrl.UrlFormattingOption.RemovePassword) + REMOVE_QUERY = cast( + _QtFormattingOptions, QUrl.UrlFormattingOption.RemoveQuery) # URL schemes supported by QtWebEngine diff --git a/requirements.txt b/requirements.txt index f10ab6f9b..01f6236c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ adblock==0.6.0 colorama==0.4.6 -importlib-resources==6.0.0 ; python_version=="3.8.*" +importlib-resources==6.0.1 ; python_version=="3.8.*" Jinja2==3.1.2 MarkupSafe==2.1.3 Pygments==2.16.1 diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index b55624b8d..55b3f5f1c 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -171,9 +171,6 @@ 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\.\.\.'), - - # https://github.com/pyinstaller/pyinstaller/pull/6903 - r"[0-9:]* INFO: Sandboxing disabled by user\.", ]) elif IS_WINDOWS: stderr_whitelist.extend([ @@ -245,66 +242,11 @@ def verify_windows_exe(exe_path: pathlib.Path) -> None: assert pe.verify_checksum() -def patch_mac_app(qt5: bool) -> None: - """Patch .app to save some space and make it signable.""" - dist_path = pathlib.Path('dist') - ver = '5' if qt5 else '6' - app_path = dist_path / 'qutebrowser.app' - - contents_path = app_path / 'Contents' - macos_path = contents_path / 'MacOS' - resources_path = contents_path / 'Resources' - pyqt_path = macos_path / f'PyQt{ver}' - - # Replace some duplicate files by symlinks - framework_path = pyqt_path / f'Qt{ver}' / 'lib' / 'QtWebEngineCore.framework' - - framework_resource_path = framework_path / 'Resources' - for file_path in framework_resource_path.iterdir(): - target = pathlib.Path(*[os.pardir] * 5, file_path.name) - if file_path.is_dir(): - shutil.rmtree(file_path) - else: - file_path.unlink() - file_path.symlink_to(target) - - 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 - - core_lib = framework_path / 'Versions' / '5' / 'QtWebEngineCore' - core_lib.unlink() - core_target = pathlib.Path(*[os.pardir] * 7, 'MacOS', 'QtWebEngineCore') - core_lib.symlink_to(core_target) - - # Move stuff around to make things signable on macOS - # See https://github.com/pyinstaller/pyinstaller/issues/6612 - pyqt_path_dest = resources_path / pyqt_path.name - shutil.move(pyqt_path, pyqt_path_dest) - pyqt_path_target = pathlib.Path("..") / pyqt_path_dest.relative_to(contents_path) - pyqt_path.symlink_to(pyqt_path_target) - - for path in macos_path.glob("Qt*"): - link_path = resources_path / path.name - target_path = pathlib.Path("..") / path.relative_to(contents_path) - link_path.symlink_to(target_path) - - -def sign_mac_app() -> None: +def verify_mac_app() -> None: """Re-sign and verify the Mac .app.""" app_path = pathlib.Path('dist') / 'qutebrowser.app' subprocess.run([ 'codesign', - '-s', '-', - '--force', - '--timestamp', - '--deep', - '--verbose', - app_path, - ], check=True) - subprocess.run([ - 'codesign', '--verify', '--strict', '--deep', @@ -341,10 +283,8 @@ def build_mac( utils.print_title("Building .app via pyinstaller") call_tox(f'pyinstaller{"-qt5" if qt5 else ""}', '-r', debug=debug) - utils.print_title("Patching .app") - patch_mac_app(qt5=qt5) - utils.print_title("Re-signing .app") - sign_mac_app() + utils.print_title("Verifying .app") + verify_mac_app() dist_path = pathlib.Path("dist") @@ -483,6 +423,7 @@ def _package_windows_single( utils.print_subtitle("Building installer...") subprocess.run(['makensis.exe', f'/DVERSION={qutebrowser.__version__}', + f'/DQT5={qt5}', 'misc/nsis/qutebrowser.nsi'], check=True) name_parts = [ diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 24122eb30..b54b22c9a 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -622,3 +622,11 @@ Feature: Various utility commands. When I open data/invalid_resource.html in a new tab Then "Ignoring invalid * URL: Invalid hostname (contains invalid characters); *" should be logged And no crash should happen + + Scenario: Keyboard focus after cross-origin navigation + When I turn on scroll logging + And I open qute://gpl in a new tab + And I run :tab-only + And I open data/scroll/simple.html + And I run :fake-key "<Space>" + Then the page should be scrolled vertically diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature index 85f68661a..e99b9af9d 100644 --- a/tests/end2end/features/qutescheme.feature +++ b/tests/end2end/features/qutescheme.feature @@ -300,3 +300,16 @@ Feature: Special qute:// pages Scenario: Open qute://gpl When I open qute://gpl Then the page should contain the plaintext "GNU GENERAL PUBLIC LICENSE" + + # qute://start + + # QtWebKit doesn't support formaction + @qtwebkit_skip + Scenario: Searching on qute://start + When I set url.searchengines to {"DEFAULT": "http://localhost:(port)/data/title.html?q={}"} + And I open qute://start + And I run :click-element id search-field + And I wait for "Entering mode KeyMode.insert *" in the log + And I press the keys "test" + And I press the key "<Enter>" + Then data/title.html?q=test should be loaded diff --git a/tests/end2end/features/test_misc_bdd.py b/tests/end2end/features/test_misc_bdd.py index 7f899b6de..570afee64 100644 --- a/tests/end2end/features/test_misc_bdd.py +++ b/tests/end2end/features/test_misc_bdd.py @@ -16,6 +16,11 @@ def load_iframe(quteproc, server, ssl_server): msg.expected = True +@bdd.when("I turn on scroll logging") +def turn_on_scroll_logging(quteproc): + quteproc.turn_on_scroll_logging(no_scroll_filtering=True) + + @bdd.then(bdd.parsers.parse('the PDF {filename} should exist in the tmpdir')) def pdf_exists(quteproc, tmpdir, filename): path = tmpdir / filename diff --git a/tests/end2end/features/test_urlmarks_bdd.py b/tests/end2end/features/test_urlmarks_bdd.py index 1b21098cd..2a7b65d8c 100644 --- a/tests/end2end/features/test_urlmarks_bdd.py +++ b/tests/end2end/features/test_urlmarks_bdd.py @@ -4,6 +4,7 @@ import os.path +import pytest import pytest_bdd as bdd from helpers import testutils @@ -11,6 +12,16 @@ from helpers import testutils bdd.scenarios('urlmarks.feature') +@pytest.fixture(autouse=True) +def clear_marks(quteproc): + """Clear all existing marks between tests.""" + yield + quteproc.send_cmd(':quickmark-del --all') + quteproc.wait_for(message="Quickmarks cleared.") + quteproc.send_cmd(':bookmark-del --all') + quteproc.wait_for(message="Bookmarks cleared.") + + def _check_marks(quteproc, quickmarks, expected, contains): """Make sure the given line does (not) exist in the bookmarks. diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature index 00fd14fb6..05d07ae98 100644 --- a/tests/end2end/features/urlmarks.feature +++ b/tests/end2end/features/urlmarks.feature @@ -84,7 +84,24 @@ Feature: quickmarks and bookmarks When I open data/numbers/5.txt And I run :bookmark-add And I run :bookmark-del http://localhost:(port)/data/numbers/5.txt - Then the bookmark file should not contain "http://localhost:*/data/numbers/5.txt " + Then the bookmark file should not contain "http://localhost:*/data/numbers/5.txt *" + + Scenario: Deleting all bookmarks + When I open data/numbers/1.txt + And I run :bookmark-add + And I open data/numbers/2.txt + And I run :bookmark-add + And I run :bookmark-del --all + Then the message "Bookmarks cleared." should be shown + And the bookmark file should not contain "http://localhost:*/data/numbers/1.txt *" + And the bookmark file should not contain "http://localhost:*/data/numbers/2.txt *" + + Scenario: Deleting all bookmarks with url + When I open data/numbers/1.txt + And I run :bookmark-add + And I run :bookmark-del --all https://example.org + Then the error "Cannot specify url and --all" should be shown + And the bookmark file should contain "http://localhost:*/data/numbers/1.txt *" Scenario: Deleting the current page's bookmark if it doesn't exist When I open data/hello.txt @@ -95,18 +112,18 @@ Feature: quickmarks and bookmarks When I open data/numbers/6.txt And I run :bookmark-add And I run :bookmark-del - Then the bookmark file should not contain "http://localhost:*/data/numbers/6.txt " + Then the bookmark file should not contain "http://localhost:*/data/numbers/6.txt *" Scenario: Toggling a bookmark When I open data/numbers/7.txt And I run :bookmark-add And I run :bookmark-add --toggle - Then the bookmark file should not contain "http://localhost:*/data/numbers/7.txt " + Then the bookmark file should not contain "http://localhost:*/data/numbers/7.txt *" Scenario: Loading a bookmark with --delete When I run :bookmark-add http://localhost:(port)/data/numbers/8.txt "eight" And I run :bookmark-load -d http://localhost:(port)/data/numbers/8.txt - Then the bookmark file should not contain "http://localhost:*/data/numbers/8.txt " + Then the bookmark file should not contain "http://localhost:*/data/numbers/8.txt *" ## quickmarks @@ -210,6 +227,20 @@ Feature: quickmarks and bookmarks And I run :quickmark-del eighteen Then the quickmark file should not contain "eighteen http://localhost:*/data/numbers/18.txt " + Scenario: Deleting all quickmarks + When I run :quickmark-add http://localhost:(port)/data/numbers/1.txt one + When I run :quickmark-add http://localhost:(port)/data/numbers/2.txt two + And I run :quickmark-del --all + Then the message "Quickmarks cleared." should be shown + And the quickmark file should not contain "one http://localhost:*/data/numbers/1.txt" + And the quickmark file should not contain "two http://localhost:*/data/numbers/2.txt" + + Scenario: Deleting all quickmarks with name + When I run :quickmark-add http://localhost:(port)/data/numbers/1.txt one + And I run :quickmark-del --all invalid + Then the error "Cannot specify name and --all" should be shown + And the quickmark file should contain "one http://localhost:*/data/numbers/1.txt" + Scenario: Deleting the current page's quickmark if it has none When I open data/hello.txt And I run :quickmark-del @@ -233,3 +264,10 @@ Feature: quickmarks and bookmarks And I run :bookmark-add And I open qute://bookmarks Then the page should contain the plaintext "Test title" + + Scenario: Following a bookmark + When I open data/numbers/1.txt in a new tab + And I run :bookmark-add + And I open qute://bookmarks + And I hint with args "links current" and follow a + Then data/numbers/1.txt should be loaded diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index a10fd5414..a3929ed7e 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -57,7 +57,7 @@ def is_ignored_lowlevel_message(message): # 'style-src' was not explicitly set, so 'default-src' is used as a # fallback. # INVALID: ", source: userscript:_qute_stylesheet (65) - '", source: userscript:_qute_stylesheet (65)', + '", source: userscript:_qute_stylesheet (*)', # Randomly started showing up on Qt 5.15.2 'QPaintDevice: Cannot destroy paint device that is being painted', diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py index 7ad898d17..f716c7443 100644 --- a/tests/end2end/fixtures/webserver_sub.py +++ b/tests/end2end/fixtures/webserver_sub.py @@ -117,7 +117,7 @@ def redirect_to(): # header to the exact string supplied. response = app.make_response('') response.status_code = HTTPStatus.FOUND - response.headers['Location'] = flask.request.args['url'].encode('utf-8') + response.headers['Location'] = flask.request.args['url'] return response diff --git a/tests/unit/extensions/test_loader.py b/tests/unit/extensions/test_loader.py index a2a99f305..fd15130ba 100644 --- a/tests/unit/extensions/test_loader.py +++ b/tests/unit/extensions/test_loader.py @@ -20,10 +20,16 @@ def test_on_walk_error(): def test_walk_normal(): - names = [info.name for info in loader.walk_components()] + names = [info.name for info in loader._walk_normal()] assert 'qutebrowser.components.scrollcommands' in names +def test_walk_pyinstaller(): + # We can't test whether we get something back without being frozen by + # PyInstaller, but at least we can test that we don't crash. + list(loader._walk_pyinstaller()) + + def test_load_component(monkeypatch): monkeypatch.setattr(objects, 'commands', {}) diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 5b173882b..541f4e4fe 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -13,8 +13,9 @@ import unittest.mock import pytest from qutebrowser.qt.core import (QDataStream, QPoint, QUrl, QByteArray, QIODevice, - QTimer, QBuffer, QFile, QProcess, QFileDevice, QLibraryInfo, Qt) + QTimer, QBuffer, QFile, QProcess, QFileDevice, QLibraryInfo, Qt, QObject) from qutebrowser.qt.gui import QColor +from qutebrowser.qt import sip from qutebrowser.utils import qtutils, utils, usertypes import overflow_test_cases @@ -1051,3 +1052,50 @@ class TestLibraryPath: def test_extract_enum_val(): value = qtutils.extract_enum_val(Qt.KeyboardModifier.ShiftModifier) assert value == 0x02000000 + + +class TestQObjRepr: + + @pytest.mark.parametrize("obj", [QObject(), object(), None]) + def test_simple(self, obj): + assert qtutils.qobj_repr(obj) == repr(obj) + + def _py_repr(self, obj): + """Get the original repr of an object, with <> stripped off. + + We do this in code instead of recreating it in tests because of output + differences between PyQt5/PyQt6 and between operating systems. + """ + r = repr(obj) + if r.startswith("<") and r.endswith(">"): + return r[1:-1] + return r + + def test_object_name(self): + obj = QObject() + obj.setObjectName("Tux") + expected = f"<{self._py_repr(obj)}, objectName='Tux'>" + assert qtutils.qobj_repr(obj) == expected + + def test_class_name(self): + obj = QTimer() + hidden = sip.cast(obj, QObject) + expected = f"<{self._py_repr(hidden)}, className='QTimer'>" + assert qtutils.qobj_repr(hidden) == expected + + def test_both(self): + obj = QTimer() + obj.setObjectName("Pomodoro") + hidden = sip.cast(obj, QObject) + expected = f"<{self._py_repr(hidden)}, objectName='Pomodoro', className='QTimer'>" + assert qtutils.qobj_repr(hidden) == expected + + def test_rich_repr(self): + class RichRepr(QObject): + def __repr__(self): + return "RichRepr()" + + obj = RichRepr() + assert repr(obj) == "RichRepr()" # sanity check + expected = "<RichRepr(), className='RichRepr'>" + assert qtutils.qobj_repr(obj) == expected @@ -61,13 +61,17 @@ setenv = PYTEST_QT_API=pyqt5 QUTE_QT_WRAPPER=PyQt5 -[testenv:bleeding] +[testenv:bleeding{,-qt5}] basepython = {env:PYTHON:python3} +# Override default PyQt6 from [testenv] setenv = - PYTEST_QT_API=pyqt5 + qt5: PYTEST_QT_API=pyqt5 + qt5: QUTE_QT_WRAPPER=PyQt5 pip_pre = true deps = -r{toxinidir}/misc/requirements/requirements-tests-bleeding.txt -commands_pre = pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt5 PyQtWebEngine +commands_pre = + qt5: pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt5 PyQtWebEngine + !qt5: pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt6 PyQt6-WebEngine commands = {envpython} -bb -m pytest {posargs:tests} # other envs @@ -187,6 +191,7 @@ passenv = APPDATA HOME PYINSTALLER_DEBUG + PYINSTALLER_COMPILE_BOOTLOADER setenv = qt5: PYINSTALLER_QT5=true deps = @@ -268,6 +273,7 @@ passenv = * # Override default PyQt6 from [testenv] setenv = qt5: QUTE_QT_WRAPPER=PyQt5 + PYINSTALLER_COMPILE_BOOTLOADER=true usedevelop = true deps = -r{toxinidir}/requirements.txt |