Merge remote-tracking branch 'upstream/main' into feat/mac_sandbox_pre_release_pyinstaller

Only conflict was the removal of support for 32bit builds in
build_release.py
This commit is contained in:
toofar 2023-08-12 13:49:01 +12:00
commit 0f2d34623c
34 changed files with 358 additions and 145 deletions

View File

@ -33,6 +33,7 @@ jobs:
args: "-f gcc" # For problem matchers
- testenv: yamllint
- testenv: actionlint
- testenv: package
steps:
- uses: actions/checkout@v3
with:

View File

@ -19,47 +19,34 @@ jobs:
toxenv: build-release-qt5
name: qt5-macos
- os: windows-2019
args: --64bit
branch: main
toxenv: build-release-qt5
name: qt5-windows-64bit
- os: windows-2019
args: --32bit
branch: main
toxenv: build-release-qt5
name: qt5-windows-32bit
name: qt5-windows
- os: macos-11
args: --debug
branch: main
toxenv: build-release-qt5
name: qt5-macos-debug
- os: windows-2019
args: --64bit --debug
args: --debug
branch: main
toxenv: build-release-qt5
name: qt5-windows-64bit-debug
- os: windows-2019
args: --32bit --debug
branch: main
toxenv: build-release-qt5
name: qt5-windows-32bit-debug
name: qt5-windows-debug
- os: macos-11
toxenv: build-release
name: macos
- os: windows-2019
args: --64bit
toxenv: build-release
name: windows-64bit
name: windows
- os: macos-11
args: --debug
toxenv: build-release
name: macos-debug
- os: windows-2019
args: --64bit --debug
args: --debug
toxenv: build-release
name: windows-64bit-debug
name: windows-debug
runs-on: "${{ matrix.os }}"
timeout-minutes: 45
steps:

View File

@ -220,6 +220,7 @@ Active
* https://nyxt.atlas.engineer/[Nyxt browser] (formerly "Next browser", Lisp, Emacs-like but also offers Vim bindings, QtWebEngine or GTK+/WebKit2 - note there was a https://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution in 2019] which was handled quite badly)
* https://vieb.dev/[Vieb] (JavaScript, Electron)
* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* https://github.com/jun7/wyeb[wyeb] (C, GTK+ with WebKit2)
* Chrome/Chromium addons:
https://vimium.github.io/[Vimium]
* Firefox addons (based on WebExtensions):
@ -227,9 +228,8 @@ Active
https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF]
* Addons for Firefox and Chrome:
https://github.com/brookhong/Surfingkeys[Surfingkeys],
https://lydell.github.io/LinkHints/[Link Hints] (hinting only)
* Addons for Safari:
https://televator.net/vimari/[Vimari]
https://lydell.github.io/LinkHints/[Link Hints] (hinting only),
https://github.com/ueokande/vimmatic[Vimmatic]
Inactive
~~~~~~~~
@ -246,7 +246,6 @@ main inspiration for qutebrowser)
* https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
* https://github.com/conformal/xombrero[xombrero] (C, GTK+ with WebKit1)
* https://github.com/linkdd/cream-browser[Cream Browser] (C, GTK+ with WebKit1)
* https://github.com/jun7/wyeb[wyeb] (C, GTK+ with WebKit2)
* Firefox addons (not based on WebExtensions or no recent activity):
http://www.vimperator.org/[Vimperator],
http://bug.5digits.org/pentadactyl/index[Pentadactyl],
@ -263,6 +262,8 @@ main inspiration for qutebrowser)
https://github.com/1995eaton/chromium-vim[cVim],
https://github.com/dcchambers/vb4c[vb4c] (fork of cVim, https://github.com/dcchambers/vb4c/issues/23#issuecomment-810694017[unmaintained]),
https://glee.github.io/[GleeBox]
* Addons for Safari:
https://televator.net/vimari/[Vimari]
License
-------

View File

@ -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
~~~~~~~
@ -202,8 +204,16 @@ Fixed
- Crash when using QtWebKit with PAC and the file has an invalid encoding.
- Crash with the "tiramisu" notification server.
- Crash when the "herbe" notification presenter doesn't start correctly.
- Crash when no notification server is installed/available.
- Warning with recent versions of the "deadd" (aka "linux notification center") notification server.
- Crash when using `:print --pdf` with a directory where its parent directory
did not exist.
- The `PyQt{5,6}.sip` version is now shown correctly in the :version|--version
output. Previously that showed the version from the standalone `sip` module
which was only set for PyQt5. (#7805)
- When a `config.py` calls `.redirect()` via a request interceptor (which is
unsupported) and supplies an invalid redirect target URL, an exception is now
raised for the `.redirect()` call instead of later inside qutebrowser.
[[v2.5.4]]
v2.5.4 (2023-03-13)

View File

@ -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.
@ -998,7 +1001,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.
@ -1007,6 +1010,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.

View File

@ -6,7 +6,7 @@ bump2version==1.0.1
certifi==2023.7.22
cffi==1.15.1
charset-normalizer==3.2.0
cryptography==41.0.2
cryptography==41.0.3
docutils==0.20.1
github3.py==4.0.1
hunter==3.6.1
@ -19,12 +19,12 @@ keyring==24.2.0
manhole==1.8.0
markdown-it-py==3.0.0
mdurl==0.1.2
more-itertools==9.1.0
more-itertools==10.1.0
packaging==23.1
pkginfo==1.9.6
ply==3.11
pycparser==2.21
Pygments==2.15.1
Pygments==2.16.1
PyJWT==2.8.0
Pympler==1.0.1
pyproject_hooks==1.0.0
@ -34,9 +34,9 @@ readme-renderer==40.0
requests==2.31.0
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==13.4.2
rich==13.5.2
SecretStorage==3.3.3
sip==6.7.10
sip==6.7.11
six==1.16.0
tomli==2.0.1
twine==4.0.2

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==23.1.0
flake8==6.0.0
flake8==6.1.0
flake8-bugbear==23.7.10
flake8-builtins==2.1.0
flake8-comprehensions==3.14.0
@ -16,8 +16,8 @@ flake8-tidy-imports==4.10.0
flake8-tuple==0.4.1
mccabe==0.7.0
pep8-naming==0.13.3
pycodestyle==2.10.0
pycodestyle==2.11.0
pydocstyle==6.3.0
pyflakes==3.0.1
pyflakes==3.1.0
six==1.16.0
snowballstemmer==2.2.0

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
chardet==5.1.0
chardet==5.2.0
diff-cover==7.7.0
importlib-resources==6.0.0
Jinja2==3.1.2
@ -9,12 +9,12 @@ MarkupSafe==2.1.3
mypy==1.4.1
mypy-extensions==1.0.0
pluggy==1.2.0
Pygments==2.15.1
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-Pygments==2.15.0.2
types-Pygments==2.16.0.0
types-PyYAML==6.0.12.11
types-setuptools==68.0.0.3
typing_extensions==4.7.1

View File

@ -4,7 +4,7 @@ astroid==2.15.6
certifi==2023.7.22
cffi==1.15.1
charset-normalizer==3.2.0
cryptography==41.0.2
cryptography==41.0.3
dill==0.3.7
github3.py==4.0.1
idna==3.4
@ -12,16 +12,16 @@ isort==5.12.0
lazy-object-proxy==1.9.0
mccabe==0.7.0
pefile==2023.2.7
platformdirs==3.9.1
platformdirs==3.10.0
pycparser==2.21
PyJWT==2.8.0
pylint==2.17.4
pylint==2.17.5
python-dateutil==2.8.2
./scripts/dev/pylint_checkers
requests==2.31.0
six==1.16.0
tomli==2.0.1
tomlkit==0.11.8
tomlkit==0.12.1
typing_extensions==4.7.1
uritemplate==4.1.1
# urllib3==2.0.4

View File

@ -6,7 +6,7 @@ charset-normalizer==3.2.0
docutils==0.20.1
idna==3.4
packaging==23.1
Pygments==2.15.1
Pygments==2.16.1
pyproject_hooks==1.0.0
pyroma==4.2
requests==2.31.0

View File

@ -11,11 +11,11 @@ importlib-metadata==6.8.0
Jinja2==3.1.2
MarkupSafe==2.1.3
packaging==23.1
Pygments==2.15.1
Pygments==2.16.1
pytz==2023.3
requests==2.31.0
snowballstemmer==2.2.0
Sphinx==7.0.1
Sphinx==7.1.2
sphinxcontrib-applehelp==1.0.4
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.1

View File

@ -13,23 +13,23 @@ execnet==2.0.2
filelock==3.12.2
Flask==2.3.2
hunter==3.6.1
hypothesis==6.82.0
hypothesis==6.82.2
idna==3.4
importlib-metadata==6.8.0
iniconfig==2.0.0
itsdangerous==2.1.2
jaraco.functools==3.8.0
jaraco.functools==3.8.1
# Jinja2==3.1.2
Mako==1.2.4
manhole==1.8.0
# MarkupSafe==2.1.3
more-itertools==9.1.0
more-itertools==10.1.0
packaging==23.1
parse==1.19.1
parse-type==0.6.2
pluggy==1.2.0
py-cpuinfo==9.0.0
Pygments==2.15.1
Pygments==2.16.1
pytest==7.4.0
pytest-bdd==6.1.1
pytest-benchmark==4.0.0

View File

@ -1,17 +1,17 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
cachetools==5.3.1
chardet==5.1.0
chardet==5.2.0
colorama==0.4.6
distlib==0.3.7
filelock==3.12.2
packaging==23.1
pip==23.2.1
platformdirs==3.9.1
platformdirs==3.10.0
pluggy==1.2.0
pyproject-api==1.5.3
setuptools==68.0.0
tomli==2.0.1
tox==4.6.4
virtualenv==20.24.1
wheel==0.41.0
virtualenv==20.24.2
wheel==0.41.1

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pathspec==0.11.1
pathspec==0.11.2
PyYAML==6.0.1
yamllint==1.32.0

View File

@ -35,7 +35,7 @@ Possible values:
import inspect
from typing import Any, Callable, Iterable
from typing import Any, Callable, Iterable, Protocol, Optional, Dict, cast
from qutebrowser.utils import qtutils
from qutebrowser.commands import command, cmdexc
@ -90,7 +90,21 @@ def check_exclusive(flags: Iterable[bool], names: Iterable[str]) -> None:
raise CommandError("Only one of {} can be given!".format(argstr))
_CmdHandlerType = Callable[..., Any]
_CmdHandlerFunc = Callable[..., Any]
class _CmdHandlerType(Protocol):
"""A qutebrowser command function, which had qute_args patched on it.
Applying @cmdutils.argument to a function will patch it with a qute_args attribute.
Below, we cast the decorated function to _CmdHandlerType to make mypy aware of this.
"""
qute_args: Optional[Dict[str, command.ArgInfo]]
def __call__(self, *args: Any, **kwargs: Any) -> Any:
...
class register: # noqa: N801,N806 pylint: disable=invalid-name
@ -118,7 +132,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
# The arguments to pass to Command.
self._kwargs = kwargs
def __call__(self, func: _CmdHandlerType) -> _CmdHandlerType:
def __call__(self, func: _CmdHandlerFunc) -> _CmdHandlerType:
"""Register the command before running the function.
Gets called when a function should be decorated.
@ -158,7 +172,8 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
# This is checked by future @cmdutils.argument calls so they fail
# (as they'd be silently ignored otherwise)
func.qute_args = None # type: ignore[attr-defined]
func = cast(_CmdHandlerType, func)
func.qute_args = None
return func
@ -210,19 +225,21 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name
self._argname = argname # The name of the argument to handle.
self._kwargs = kwargs # Valid ArgInfo members.
def __call__(self, func: _CmdHandlerType) -> _CmdHandlerType:
def __call__(self, func: _CmdHandlerFunc) -> _CmdHandlerType:
funcname = func.__name__
if self._argname not in inspect.signature(func).parameters:
raise ValueError("{} has no argument {}!".format(funcname,
self._argname))
func = cast(_CmdHandlerType, func)
if not hasattr(func, 'qute_args'):
func.qute_args = {} # type: ignore[attr-defined]
func.qute_args = {}
elif func.qute_args is None:
raise ValueError("@cmdutils.argument got called above (after) "
"@cmdutils.register for {}!".format(funcname))
arginfo = command.ArgInfo(**self._kwargs)
func.qute_args[self._argname] = arginfo # type: ignore[attr-defined]
func.qute_args[self._argname] = arginfo
return func

View File

@ -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.3 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, 3)
):
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

View File

@ -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))

View File

@ -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):

View File

@ -10,7 +10,7 @@ from qutebrowser.qt.webenginecore import (QWebEngineUrlRequestInterceptor,
from qutebrowser.config import websettings, config
from qutebrowser.browser import shared
from qutebrowser.utils import debug, log
from qutebrowser.utils import debug, log, qtutils
from qutebrowser.extensions import interceptors
from qutebrowser.misc import objects
@ -35,6 +35,11 @@ class WebEngineRequest(interceptors.Request):
if self._webengine_info is None:
raise interceptors.RedirectException("Request improperly initialized.")
try:
qtutils.ensure_valid(url)
except qtutils.QtValueError as e:
raise interceptors.RedirectException(f"Redirect to invalid URL: {e}")
# Redirecting a request that contains payload data is not allowed.
# To be safe, abort on any request not in a whitelist.
verb = self._webengine_info.requestMethod()

View File

@ -113,6 +113,9 @@ class DBusError(Error):
# https://crashes.qutebrowser.org/view/de62220a
# after "Notification daemon did quit!"
"org.freedesktop.DBus.Error.UnknownObject",
# notmuch-sha1-ef7b6e9e79e5f2f6cba90224122288895c1fe0d8
"org.freedesktop.DBus.Error.ServiceUnknown",
}
def __init__(self, msg: QDBusMessage) -> None:
@ -856,12 +859,15 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
log.misc.debug(f"Enabling quirks {quirks}")
self._quirks = quirks
expected_spec_version = self._quirks.spec_version or self.SPEC_VERSION
if spec_version != expected_spec_version:
expected_spec_versions = [self.SPEC_VERSION]
if self._quirks.spec_version is not None:
expected_spec_versions.append(self._quirks.spec_version)
if spec_version not in expected_spec_versions:
log.misc.warning(
f"Notification server ({name} {ver} by {vendor}) implements "
f"spec {spec_version}, but {expected_spec_version} was expected. "
f"If {name} is up to date, please report a qutebrowser bug.")
f"spec {spec_version}, but {'/'.join(expected_spec_versions)} was "
f"expected. If {name} is up to date, please report a qutebrowser bug.")
# https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html
icon_key_overrides = {

View File

@ -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:

View File

@ -316,6 +316,8 @@ class TabbedBrowser(QWidget):
fields['id'] = self._win_id
title = title_format.format(**fields)
# prevent hanging WMs and similar issues with giant URLs
title = utils.elide(title, 1024)
self._window().setWindowTitle(title)

View File

@ -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

View File

@ -381,7 +381,6 @@ class ModuleInfo:
def _create_module_info() -> Dict[str, ModuleInfo]:
packages = [
('sip', ['SIP_VERSION_STR']),
('colorama', ['VERSION', '__version__']),
('jinja2', ['__version__']),
('pygments', ['__version__']),
@ -395,9 +394,13 @@ def _create_module_info() -> Dict[str, ModuleInfo]:
('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']),
('PyQt5.QtWebKitWidgets', []),
('PyQt5.sip', ['SIP_VERSION_STR']),
]
elif machinery.IS_QT6:
packages.append(('PyQt6.QtWebEngineCore', ['PYQT_WEBENGINE_VERSION_STR']))
packages += [
('PyQt6.QtWebEngineCore', ['PYQT_WEBENGINE_VERSION_STR']),
('PyQt6.sip', ['SIP_VERSION_STR']),
]
else:
raise utils.Unreachable()

View File

@ -5,7 +5,7 @@ colorama==0.4.6
importlib-resources==6.0.0 ; python_version=="3.8.*"
Jinja2==3.1.2
MarkupSafe==2.1.3
Pygments==2.15.1
Pygments==2.16.1
PyYAML==6.0.1
zipp==3.16.2
# Unpinned due to recompile_requirements.py limitations

View File

@ -282,7 +282,7 @@ def build_mac(
gh_token=gh_token)
utils.print_title("Building .app via pyinstaller")
call_tox(f'pyinstaller-64bit{"-qt5" if qt5 else ""}', '-r', debug=debug)
call_tox(f'pyinstaller{"-qt5" if qt5 else ""}', '-r', debug=debug)
utils.print_title("Verifying .app")
verify_mac_app()
@ -328,18 +328,14 @@ def build_mac(
]
def _get_windows_python_path(x64: bool) -> pathlib.Path:
def _get_windows_python_path() -> pathlib.Path:
"""Get the path to Python.exe on Windows."""
parts = str(sys.version_info.major), str(sys.version_info.minor)
ver = ''.join(parts)
dot_ver = '.'.join(parts)
if x64:
path = rf'SOFTWARE\Python\PythonCore\{dot_ver}\InstallPath'
fallback = pathlib.Path('C:', f'Python{ver}', 'python.exe')
else:
path = rf'SOFTWARE\WOW6432Node\Python\PythonCore\{dot_ver}-32\InstallPath'
fallback = pathlib.Path('C:', f'Python{ver}-32', 'python.exe')
path = rf'SOFTWARE\Python\PythonCore\{dot_ver}\InstallPath'
fallback = pathlib.Path('C:', f'Python{ver}', 'python.exe')
try:
key = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, path)
@ -349,47 +345,39 @@ def _get_windows_python_path(x64: bool) -> pathlib.Path:
def _build_windows_single(
*, x64: bool,
*,
qt5: bool,
skip_packaging: bool,
debug: bool,
) -> List[Artifact]:
"""Build on Windows for a single architecture."""
human_arch = '64-bit' if x64 else '32-bit'
utils.print_title(f"Running pyinstaller {human_arch}")
"""Build on Windows for a single build type."""
utils.print_title("Running pyinstaller")
dist_path = pathlib.Path("dist")
arch = "x64" if x64 else "x86"
out_path = dist_path / f'qutebrowser-{qutebrowser.__version__}-{arch}'
out_path = dist_path / f'qutebrowser-{qutebrowser.__version__}'
_maybe_remove(out_path)
python = _get_windows_python_path(x64=x64)
suffix = "64bit" if x64 else "32bit"
if qt5:
# FIXME:qt6 does this regress 391623d5ec983ecfc4512c7305c4b7a293ac3872?
suffix += "-qt5"
call_tox(f'pyinstaller-{suffix}', '-r', python=python, debug=debug)
python = _get_windows_python_path()
# FIXME:qt6 does this regress 391623d5ec983ecfc4512c7305c4b7a293ac3872?
suffix = "-qt5" if qt5 else ""
call_tox(f'pyinstaller{suffix}', '-r', python=python, debug=debug)
out_pyinstaller = dist_path / "qutebrowser"
shutil.move(out_pyinstaller, out_path)
exe_path = out_path / 'qutebrowser.exe'
utils.print_title(f"Verifying {human_arch} exe")
utils.print_title("Verifying exe")
verify_windows_exe(exe_path)
utils.print_title(f"Running {human_arch} smoke test")
utils.print_title("Running smoke test")
smoke_test(exe_path, debug=debug, qt5=qt5)
if skip_packaging:
return []
utils.print_title(f"Packaging {human_arch}")
utils.print_title("Packaging")
return _package_windows_single(
nsis_flags=[] if x64 else ['/DX86'],
out_path=out_path,
filename_arch='amd64' if x64 else 'win32',
desc_arch=human_arch,
desc_suffix='' if x64 else ' (only for 32-bit Windows!)',
debug=debug,
qt5=qt5,
)
@ -398,8 +386,6 @@ def _build_windows_single(
def build_windows(
*, gh_token: str,
skip_packaging: bool,
only_32bit: bool,
only_64bit: bool,
qt5: bool,
debug: bool,
) -> List[Artifact]:
@ -410,37 +396,23 @@ def build_windows(
utils.print_title("Building Windows binaries")
artifacts = []
from scripts.dev import gen_versioninfo
utils.print_title("Updating VersionInfo file")
gen_versioninfo.main()
if not only_32bit:
artifacts += _build_windows_single(
x64=True,
artifacts = [
_build_windows_single(
skip_packaging=skip_packaging,
debug=debug,
qt5=qt5,
)
if not only_64bit and qt5:
artifacts += _build_windows_single(
x64=False,
skip_packaging=skip_packaging,
debug=debug,
qt5=qt5,
)
),
]
return artifacts
def _package_windows_single(
*,
nsis_flags: List[str],
out_path: pathlib.Path,
desc_arch: str,
desc_suffix: str,
filename_arch: str,
debug: bool,
qt5: bool,
) -> List[Artifact]:
@ -448,15 +420,14 @@ def _package_windows_single(
artifacts = []
dist_path = pathlib.Path("dist")
utils.print_subtitle(f"Building {desc_arch} installer...")
utils.print_subtitle("Building installer...")
subprocess.run(['makensis.exe',
f'/DVERSION={qutebrowser.__version__}', *nsis_flags,
f'/DVERSION={qutebrowser.__version__}',
'misc/nsis/qutebrowser.nsi'], check=True)
name_parts = [
'qutebrowser',
str(qutebrowser.__version__),
filename_arch,
]
if debug:
name_parts.append('debug')
@ -467,16 +438,15 @@ def _package_windows_single(
artifacts.append(Artifact(
path=dist_path / name,
mimetype='application/vnd.microsoft.portable-executable',
description=f'Windows {desc_arch} installer{desc_suffix}',
description='Windows installer',
))
utils.print_subtitle(f"Zipping {desc_arch} standalone...")
utils.print_subtitle("Zipping standalone...")
zip_name_parts = [
'qutebrowser',
str(qutebrowser.__version__),
'windows',
'standalone',
filename_arch,
]
if debug:
zip_name_parts.append('debug')
@ -489,7 +459,7 @@ def _package_windows_single(
artifacts.append(Artifact(
path=zip_path,
mimetype='application/zip',
description=f'Windows {desc_arch} standalone{desc_suffix}',
description='Windows standalone',
))
return artifacts
@ -660,10 +630,6 @@ def main() -> None:
help="Skip confirmation before uploading.")
parser.add_argument('--skip-packaging', action='store_true', required=False,
help="Skip Windows installer/zip generation or macOS DMG.")
parser.add_argument('--32bit', action='store_true', required=False,
help="Skip Windows 64 bit build.", dest='only_32bit')
parser.add_argument('--64bit', action='store_true', required=False,
help="Skip Windows 32 bit build.", dest='only_64bit')
parser.add_argument('--debug', action='store_true', required=False,
help="Build a debug build.")
parser.add_argument('--qt5', action='store_true', required=False,
@ -694,8 +660,6 @@ def main() -> None:
artifacts = build_windows(
gh_token=gh_token,
skip_packaging=args.skip_packaging,
only_32bit=args.only_32bit,
only_64bit=args.only_64bit,
qt5=args.qt5,
debug=args.debug,
)

View File

@ -300,3 +300,14 @@ 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
Scenario: Seaching 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

View File

@ -4,6 +4,7 @@
import os.path
import pytest
import pytest_bdd as bdd
from helpers import testutils
@ -11,6 +12,14 @@ 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.send_cmd(':bookmark-del --all')
def _check_marks(quteproc, quickmarks, expected, contains):
"""Make sure the given line does (not) exist in the bookmarks.

View File

@ -86,6 +86,23 @@ Feature: quickmarks and bookmarks
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 "
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
And I run :bookmark-del
@ -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

View File

@ -937,6 +937,7 @@ def test_restart(request, quteproc_new):
# If the new process hangs, this will hang too.
# Still better than just ignoring it, so we can fix it if something is broken.
os.waitpid(pid, 0) # pid, options... positional-only :(
except ChildProcessError:
# Already gone
except (ChildProcessError, PermissionError):
# Already gone. Even if not documented, Windows seems to raise PermissionError
# here...
pass

View File

@ -150,7 +150,7 @@ def partial_compare(val1, val2, *, indent=0):
if val2 is Ellipsis:
print_i("Ignoring ellipsis comparison", indent, error=True)
return PartialCompareOutcome()
elif type(val1) != type(val2): # pylint: disable=unidiomatic-typecheck
elif type(val1) is not type(val2):
outcome = PartialCompareOutcome(
"Different types ({}, {}) -> False".format(type(val1).__name__,
type(val2).__name__))

View File

@ -6,12 +6,15 @@
import pytest
import pytest_mock
pytest.importorskip('qutebrowser.qt.webenginecore')
pytest.importorskip("qutebrowser.qt.webenginecore")
from qutebrowser.qt.core import QUrl, QByteArray
from qutebrowser.qt.webenginecore import QWebEngineUrlRequestInfo
from qutebrowser.browser.webengine import interceptor
from qutebrowser.extensions import interceptors
from qutebrowser.utils import qtutils
from helpers import testutils
@ -19,10 +22,12 @@ from helpers import testutils
def test_no_missing_resource_types():
request_interceptor = interceptor.RequestInterceptor()
qb_keys = set(request_interceptor._resource_types.keys())
qt_keys = set(testutils.enum_members(
QWebEngineUrlRequestInfo,
QWebEngineUrlRequestInfo.ResourceType,
).values())
qt_keys = set(
testutils.enum_members(
QWebEngineUrlRequestInfo,
QWebEngineUrlRequestInfo.ResourceType,
).values()
)
assert qt_keys == qb_keys
@ -30,3 +35,85 @@ def test_resource_type_values():
request_interceptor = interceptor.RequestInterceptor()
for qt_value, qb_item in request_interceptor._resource_types.items():
assert qtutils.extract_enum_val(qt_value) == qb_item.value
@pytest.fixture
def we_request( # a shrubbery!
mocker: pytest_mock.MockerFixture,
) -> interceptor.WebEngineRequest:
qt_info = mocker.Mock(spec=QWebEngineUrlRequestInfo)
qt_info.requestMethod.return_value = QByteArray(b"GET")
first_party_url = QUrl("https://firstparty.example.org/")
request_url = QUrl("https://request.example.org/")
return interceptor.WebEngineRequest(
first_party_url=first_party_url,
request_url=request_url,
webengine_info=qt_info,
)
def test_block(we_request: interceptor.WebEngineRequest):
assert not we_request.is_blocked
we_request.block()
assert we_request.is_blocked
class TestRedirect:
REDIRECT_URL = QUrl("https://redirect.example.com/")
def test_redirect(self, we_request: interceptor.WebEngineRequest):
assert not we_request._redirected
we_request.redirect(self.REDIRECT_URL)
assert we_request._redirected
we_request._webengine_info.redirect.assert_called_once_with(self.REDIRECT_URL)
def test_twice(self, we_request: interceptor.WebEngineRequest):
we_request.redirect(self.REDIRECT_URL)
with pytest.raises(
interceptors.RedirectException,
match=r"Request already redirected.",
):
we_request.redirect(self.REDIRECT_URL)
we_request._webengine_info.redirect.assert_called_once_with(self.REDIRECT_URL)
def test_invalid_method(self, we_request: interceptor.WebEngineRequest):
we_request._webengine_info.requestMethod.return_value = QByteArray(b"POST")
with pytest.raises(
interceptors.RedirectException,
match=(
r"Request method b'POST' for https://request.example.org/ does not "
r"support redirection."
),
):
we_request.redirect(self.REDIRECT_URL)
assert not we_request._webengine_info.redirect.called
def test_invalid_method_ignore_unsupported(
self,
we_request: interceptor.WebEngineRequest,
caplog: pytest.LogCaptureFixture,
):
we_request._webengine_info.requestMethod.return_value = QByteArray(b"POST")
we_request.redirect(self.REDIRECT_URL, ignore_unsupported=True)
assert caplog.messages == [
"Request method b'POST' for https://request.example.org/ does not support "
"redirection."
]
assert not we_request._webengine_info.redirect.called
def test_improperly_initialized(self, we_request: interceptor.WebEngineRequest):
we_request._webengine_info = None
with pytest.raises(
interceptors.RedirectException,
match=r"Request improperly initialized.",
):
we_request.redirect(self.REDIRECT_URL)
def test_invalid_url(self, we_request: interceptor.WebEngineRequest):
url = QUrl()
assert not url.isValid()
with pytest.raises(
interceptors.RedirectException,
match=r"Redirect to invalid URL: PyQt\d\.QtCore\.QUrl\(''\) is not valid",
):
we_request.redirect(url)

View File

@ -644,8 +644,8 @@ class TestModuleVersions:
assert version._module_versions() == expected
@pytest.mark.parametrize('module, idx, expected', [
('colorama', 1, 'colorama: no'),
('adblock', 5, 'adblock: no'),
('colorama', 0, 'colorama: no'),
('adblock', 4, 'adblock: no'),
])
def test_missing_module(self, module, idx, expected, import_fake):
"""Test with a module missing.
@ -693,11 +693,11 @@ class TestModuleVersions:
assert not mod_info.is_usable()
expected = f"adblock: {fake_version} (< {mod_info.min_version}, outdated)"
assert version._module_versions()[5] == expected
assert version._module_versions()[4] == expected
@pytest.mark.parametrize('attribute, expected_modules', [
('VERSION', ['colorama']),
('SIP_VERSION_STR', ['sip']),
('SIP_VERSION_STR', ['PyQt5.sip', 'PyQt6.sip']),
(None, []),
])
def test_version_attribute(self, attribute, expected_modules, import_fake):

11
tox.ini
View File

@ -181,7 +181,7 @@ commands =
{envpython} scripts/dev/check_doc_changes.py {posargs}
{envpython} scripts/asciidoc2html.py {posargs}
[testenv:pyinstaller-{64bit,32bit}{,-qt5}]
[testenv:pyinstaller{,-qt5}]
basepython = {env:PYTHON:python3}
passenv =
APPDATA
@ -282,3 +282,12 @@ deps =
commands =
!qt5: {envpython} {toxinidir}/scripts/dev/build_release.py {posargs}
qt5: {envpython} {toxinidir}/scripts/dev/build_release.py --qt5 {posargs}
[testenv:package]
basepython = {env:PYTHON:python3}
setenv =
PYTHONWARNINGS=error,default:pkg_resources is deprecated as an API.:DeprecationWarning
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-dev.txt
commands = {envpython} -m build