summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bumpversion.cfg2
-rw-r--r--.github/workflows/bleeding.yml2
-rw-r--r--.gitignore1
-rw-r--r--doc/changelog.asciidoc57
-rw-r--r--doc/help/commands.asciidoc1
-rw-r--r--doc/help/configuring.asciidoc1
-rw-r--r--doc/help/settings.asciidoc10
-rw-r--r--doc/qutebrowser.1.asciidoc3
-rwxr-xr-xmisc/nsis/install.nsh3
-rw-r--r--misc/org.qutebrowser.qutebrowser.appdata.xml1
-rw-r--r--misc/org.qutebrowser.qutebrowser.desktop2
-rw-r--r--misc/requirements/requirements-check-manifest.txt8
-rw-r--r--misc/requirements/requirements-dev.txt24
-rw-r--r--misc/requirements/requirements-flake8.txt11
-rw-r--r--misc/requirements/requirements-mypy.txt14
-rw-r--r--misc/requirements/requirements-pyinstaller.txt2
-rw-r--r--misc/requirements/requirements-pylint.txt14
-rw-r--r--misc/requirements/requirements-pyqt-5.15.txt4
-rw-r--r--misc/requirements/requirements-pyqt-pyinstaller.txt7
-rw-r--r--misc/requirements/requirements-pyqt-pyinstaller.txt-raw2
-rw-r--r--misc/requirements/requirements-pyqt.txt4
-rw-r--r--misc/requirements/requirements-pyroma.txt6
-rw-r--r--misc/requirements/requirements-sphinx.txt16
-rw-r--r--misc/requirements/requirements-tests-bleeding.txt2
-rw-r--r--misc/requirements/requirements-tests.txt28
-rw-r--r--misc/requirements/requirements-tests.txt-raw3
-rw-r--r--misc/requirements/requirements-tox.txt16
-rw-r--r--misc/requirements/requirements-yamllint.txt2
-rwxr-xr-xmisc/userscripts/password_fill6
-rw-r--r--qutebrowser/__init__.py2
-rw-r--r--qutebrowser/browser/commands.py27
-rw-r--r--qutebrowser/browser/webengine/notification.py4
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py17
-rw-r--r--qutebrowser/browser/webkit/webkittab.py21
-rw-r--r--qutebrowser/components/misccommands.py5
-rw-r--r--qutebrowser/config/configdata.yml23
-rw-r--r--qutebrowser/extensions/loader.py36
-rw-r--r--qutebrowser/html/settings.html150
-rw-r--r--qutebrowser/keyinput/basekeyparser.py3
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py5
-rw-r--r--qutebrowser/misc/earlyinit.py14
-rw-r--r--qutebrowser/misc/guiprocess.py6
-rw-r--r--qutebrowser/qutebrowser.py25
-rw-r--r--qutebrowser/utils/resources.py2
-rw-r--r--qutebrowser/utils/utils.py7
-rw-r--r--qutebrowser/utils/version.py2
-rw-r--r--requirements.txt10
-rwxr-xr-xscripts/dev/build_release.py2
-rw-r--r--scripts/dev/recompile_requirements.py11
-rw-r--r--tests/conftest.py70
-rw-r--r--tests/end2end/conftest.py2
-rw-r--r--tests/end2end/data/hints/html/README.md2
-rw-r--r--tests/end2end/data/hints/html/invisible.html (renamed from tests/end2end/data/hints/invisible.html)2
-rw-r--r--tests/end2end/features/hints.feature5
-rw-r--r--tests/end2end/features/search.feature20
-rw-r--r--tests/end2end/features/tabs.feature58
-rw-r--r--tests/end2end/test_hints_html.py15
-rw-r--r--tests/unit/extensions/test_loader.py8
-rw-r--r--tests/unit/keyinput/test_basekeyparser.py11
-rw-r--r--tests/unit/misc/test_guiprocess.py14
-rw-r--r--tests/unit/test_qutebrowser.py60
-rw-r--r--tests/unit/utils/test_urlmatch.py157
-rw-r--r--tests/unit/utils/test_version.py21
-rw-r--r--tox.ini4
64 files changed, 788 insertions, 285 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index e1e31afc5..cf1c019f7 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 2.3.1
+current_version = 2.4.0
commit = True
message = Release v{new_version}
tag = True
diff --git a/.github/workflows/bleeding.yml b/.github/workflows/bleeding.yml
index 766f535d7..435141e56 100644
--- a/.github/workflows/bleeding.yml
+++ b/.github/workflows/bleeding.yml
@@ -58,7 +58,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
- python-version: 3.9
+ python-version: "3.10"
- name: Get asciidoc
uses: actions/checkout@v2
with:
diff --git a/.gitignore b/.gitignore
index 31c4ca3b7..ccfc12ccb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,6 +49,7 @@ TODO
/scripts/testbrowser/cpp/webengine/testbrowser
/scripts/testbrowser/cpp/webengine/.qmake.stash
/scripts/dev/pylint_checkers/qute_pylint.egg-info
+/scripts/dev/pylint_checkers/build
/misc/file_version_info.txt
/doc/extapi/_build
/misc/nsis/include
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index c17f35eec..aed128a51 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -15,10 +15,65 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
+[[v2.5.0]]
+v2.5.0 (unreleased)
+-------------------
+
+Changed
+~~~~~~~
+
+- Improved message if a spawned process wasn't found and a Flatpak container is
+ in use.
+- The `:tab-move` command now takes `start` and `end` as `index` to move a tab
+ to the first/last position.
+- Tests now automatically pick the backend (QtWebKit/QtWebEngine) based on
+ what's available. The `QUTE_BDD_WEBENGINE` environment variable and
+ `--qute-bdd-webengine` argument got replaced by `QUTE_TESTS_BACKEND` and
+ `--qute-backend` respectively, which can be set to either `webengine` or
+ `webkit`.
+- Using `:tab-give` or `:tab-take` on the last tab in a window now always
+ closes that window, no matter what `tabs.last_close` is set to.
+- Redesigned `qute://settings` (`:set`) page with buttons for options with
+ fixed values.
+
+Added
+~~~~~
+
+- New `input.match_counts` option which allows to turn off count matching for
+ more emacs-like bindings.
+
+Fixed
+~~~~~
+
+- When `search.incremental` is disabled, searching using `/text` followed by a
+ backwards search via `?text` (or vice-versa) now correctly changes the search
+ direction.
+
+[[v2.4.1]]
+v2.4.1 (unreleased)
+-------------------
+
+Fixed
+~~~~~
+
+- Speculative fix for an immediate crash at start with the macOS/Windows
+ binaries (in certain rare environments).
+- Speculative fix for a qutebrowser crash when the notification daemon crashes
+ while showing the notification.
+- Fix crash when using `:screenshot` with an invalid `--rect` argument.
+
[[v2.4.0]]
-v2.4.0 (unreleased)
+v2.4.0 (2021-10-21)
-------------------
+Security
+~~~~~~~~
+
+- **CVE-2021-41146**: Fix arbitrary command execution on Windows via URL handler
+ argument injection. See the
+ https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-vw27-fwjf-5qxm[security advisory]
+ for details.
+
Added
~~~~~
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index 8c11e15cc..442c136a7 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -1431,6 +1431,7 @@ If neither is given, move it to the first position.
==== positional arguments
* +'index'+: `+` or `-` to move relative to the current tab by count, or a default of 1 space.
A tab index to move to that index.
+ `start` and `end` to move to the start and the end.
==== count
diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc
index 23894ddc4..552145023 100644
--- a/doc/help/configuring.asciidoc
+++ b/doc/help/configuring.asciidoc
@@ -412,6 +412,7 @@ Pre-built colorschemes
- https://github.com/dracula/qutebrowser-dracula-theme[Dracula]
- https://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized]
- https://github.com/morhetz/gruvbox[gruvbox]: https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[The-Compiler], https://gitlab.com/shaneyost/dots-popos-september-2020/-/blob/master/qutebrowser/config.py[Shane Yost]
+- https://www.opencode.net/wakellor957/qb-breath/-/blob/main/qb-breath.py[Manjaro Breath-like]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 60c229078..4ca5c2517 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -269,6 +269,7 @@
|<<input.insert_mode.leave_on_load,input.insert_mode.leave_on_load>>|Leave insert mode when starting a new page load.
|<<input.insert_mode.plugins,input.insert_mode.plugins>>|Switch to insert mode when clicking flash and other plugins.
|<<input.links_included_in_focus_chain,input.links_included_in_focus_chain>>|Include hyperlinks in the keyboard focus chain when tabbing.
+|<<input.match_counts,input.match_counts>>|Interpret number prefixes as counts for bindings.
|<<input.media_keys,input.media_keys>>|Whether the underlying Chromium should handle media keys.
|<<input.mouse.back_forward_buttons,input.mouse.back_forward_buttons>>|Enable back and forward buttons on the mouse.
|<<input.mouse.rocker_gestures,input.mouse.rocker_gestures>>|Enable Opera-like mouse rocker gestures.
@@ -3557,6 +3558,15 @@ Type: <<types,Bool>>
Default: +pass:[true]+
+[[input.match_counts]]
+=== input.match_counts
+Interpret number prefixes as counts for bindings.
+This enables for vi-like bindings that can be prefixed with a number to indicate a count. Disabling it allows for emacs-like bindings where number keys are passed through (according to `input.forward_unbound_keys`) instead.
+
+Type: <<types,Bool>>
+
+Default: +pass:[true]+
+
[[input.media_keys]]
=== input.media_keys
Whether the underlying Chromium should handle media keys.
diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc
index 8db231add..bc312f108 100644
--- a/doc/qutebrowser.1.asciidoc
+++ b/doc/qutebrowser.1.asciidoc
@@ -65,6 +65,9 @@ show it.
*--desktop-file-name* 'DESKTOP_FILE_NAME'::
Set the base name of the desktop entry for this application. Used to set the app_id under Wayland. See https://doc.qt.io/qt-5/qguiapplication.html#desktopFileName-prop
+*--untrusted-args*::
+ Mark all following arguments as untrusted, which enforces that they are URLs/search terms (and not flags or commands)
+
=== debug arguments
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
Override the configured console loglevel
diff --git a/misc/nsis/install.nsh b/misc/nsis/install.nsh
index f29a0a9a8..9f0cdf446 100755
--- a/misc/nsis/install.nsh
+++ b/misc/nsis/install.nsh
@@ -351,13 +351,12 @@ Section "Register with Windows" SectionWindowsRegister
!insertmacro UpdateRegDWORD SHCTX "SOFTWARE\Classes\$2" "EditFlags" 0x00000002
!insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\DefaultIcon" "" "$1,0"
!insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell" "" "open"
- !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\command" "" "$\"$1$\" $\"%1$\""
+ !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\command" "" "$\"$1$\" --untrusted-args $\"%1$\""
!insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\ddeexec" "" ""
StrCmp $2 "${PRODUCT_NAME}HTML" 0 +4
StrCpy $2 "${PRODUCT_NAME}URL"
StrCpy $3 "${PRODUCT_NAME} URL"
Goto WriteRegHandler
- !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2" "URL Protocol" ""
${endif}
SectionEnd
diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml
index 7c382cbb3..9930514d0 100644
--- a/misc/org.qutebrowser.qutebrowser.appdata.xml
+++ b/misc/org.qutebrowser.qutebrowser.appdata.xml
@@ -44,6 +44,7 @@
</content_rating>
<releases>
<!-- Add new releases here -->
+<release version="2.4.0" date="2021-10-21"/>
<release version="2.3.1" date="2021-07-28"/>
<release version="2.3.0" date="2021-06-28"/>
<release version="2.2.3" date="2021-06-01"/>
diff --git a/misc/org.qutebrowser.qutebrowser.desktop b/misc/org.qutebrowser.qutebrowser.desktop
index 52144b3c5..d999496ee 100644
--- a/misc/org.qutebrowser.qutebrowser.desktop
+++ b/misc/org.qutebrowser.qutebrowser.desktop
@@ -45,7 +45,7 @@ Comment[it]= Un browser web vim-like utilizzabile da tastiera basato su PyQt5
Icon=qutebrowser
Type=Application
Categories=Network;WebBrowser;
-Exec=qutebrowser %u
+Exec=qutebrowser --untrusted-args %u
Terminal=false
StartupNotify=true
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute;
diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt
index 9a783f8b2..c61218ba3 100644
--- a/misc/requirements/requirements-check-manifest.txt
+++ b/misc/requirements/requirements-check-manifest.txt
@@ -2,8 +2,8 @@
build==0.7.0
check-manifest==0.47
-packaging==21.0
-pep517==0.11.0
-pyparsing==2.4.7
+packaging==21.3
+pep517==0.12.0
+pyparsing==3.0.6
toml==0.10.2
-tomli==1.2.1
+tomli==1.2.2
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index 000ed39aa..0dd45cebc 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -2,25 +2,25 @@
bump2version==1.0.1
certifi==2021.10.8
-cffi==1.14.6
-charset-normalizer==2.0.6
-cryptography==35.0.0
+cffi==1.15.0
+charset-normalizer==2.0.8
+cryptography==36.0.0
Deprecated==1.2.13
-github3.py==2.0.0
+github3.py==3.0.0
hunter==3.3.8
-idna==3.2
+idna==3.3
jwcrypto==1.0
manhole==1.8.0
-packaging==21.0
-pycparser==2.20
+packaging==21.3
+pycparser==2.21
Pympler==0.9
-pyparsing==2.4.7
-PyQt-builder==1.11.0
+pyparsing==3.0.6
+PyQt-builder==1.12.2
python-dateutil==2.8.2
requests==2.26.0
-sip==6.2.0
+sip==6.4.0
six==1.16.0
toml==0.10.2
-uritemplate==4.0.0
+uritemplate==4.1.1
# urllib3==1.26.7
-wrapt==1.13.1
+wrapt==1.13.3
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index 9d5c0e170..1d1f5eebc 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -1,11 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==21.2.0
-cached-property==1.5.2
-flake8==4.0.0
-flake8-bugbear==21.9.2
+flake8==4.0.1
+flake8-bugbear==21.11.28
flake8-builtins==1.5.3
-flake8-comprehensions==2.3.0
+flake8-comprehensions==3.7.0
flake8-copyright==0.2.2
flake8-debugger==4.0.0
flake8-deprecated==1.3
@@ -14,7 +13,7 @@ flake8-future-import==0.4.6
flake8-mock==0.3
flake8-polyfill==1.0.2
flake8-string-format==0.3.0
-flake8-tidy-imports==3.0.0
+flake8-tidy-imports==4.5.0
flake8-tuple==0.4.1
mccabe==0.6.1
pep8-naming==0.12.1
@@ -22,4 +21,4 @@ pycodestyle==2.8.0
pydocstyle==6.1.1
pyflakes==2.4.0
six==1.16.0
-snowballstemmer==2.1.0
+snowballstemmer==2.2.0
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index e3a05eac7..ce64972b3 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -2,12 +2,12 @@
chardet==4.0.0
diff-cover==6.4.2
-importlib-metadata==4.8.1
-importlib-resources==5.2.2
+importlib-metadata==4.8.2
+importlib-resources==5.4.0
inflect==5.3.0
-Jinja2==3.0.2
+Jinja2==3.0.3
jinja2-pluralize==0.3.0
-lxml==4.6.3
+lxml==4.6.4
MarkupSafe==2.0.1
mypy==0.910
mypy-extensions==0.4.3
@@ -15,7 +15,7 @@ pluggy==1.0.0
Pygments==2.10.0
PyQt5-stubs==5.15.2.0
toml==0.10.2
-types-dataclasses==0.1.7
-types-PyYAML==5.4.10
-typing-extensions==3.10.0.2
+types-dataclasses==0.6.1
+types-PyYAML==6.0.1
+typing_extensions==4.0.0
zipp==3.6.0
diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt
index 81b66393b..9a53c11cd 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.2
-pyinstaller==4.5.1
+pyinstaller==4.7
pyinstaller-hooks-contrib==2021.3
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index 9dc56ea29..c26af6406 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -2,25 +2,25 @@
astroid==2.3.3 # rq.filter: < 2.4
certifi==2021.10.8
-cffi==1.14.6
-charset-normalizer==2.0.6
-cryptography==35.0.0
+cffi==1.15.0
+charset-normalizer==2.0.8
+cryptography==36.0.0
Deprecated==1.2.13
future==0.18.2
-github3.py==2.0.0
-idna==3.2
+github3.py==3.0.0
+idna==3.3
isort==4.3.21
jwcrypto==1.0
lazy-object-proxy==1.4.3
mccabe==0.6.1
pefile==2021.9.3
-pycparser==2.20
+pycparser==2.21
pylint==2.4.4 # rq.filter: < 2.5
python-dateutil==2.8.2
./scripts/dev/pylint_checkers
requests==2.26.0
six==1.16.0
typed-ast==1.4.3 ; python_version<"3.8"
-uritemplate==4.0.0
+uritemplate==4.1.1
# urllib3==1.26.7
wrapt==1.11.2
diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt
index 8b7a53c44..3a3110c8b 100644
--- a/misc/requirements/requirements-pyqt-5.15.txt
+++ b/misc/requirements/requirements-pyqt-5.15.txt
@@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-PyQt5==5.15.4 # rq.filter: < 5.16
+PyQt5==5.15.6 # rq.filter: < 5.16
PyQt5-Qt5==5.15.2
PyQt5-sip==12.9.0
-PyQtWebEngine==5.15.4 # rq.filter: < 5.16
+PyQtWebEngine==5.15.5 # rq.filter: < 5.16
PyQtWebEngine-Qt5==5.15.2
diff --git a/misc/requirements/requirements-pyqt-pyinstaller.txt b/misc/requirements/requirements-pyqt-pyinstaller.txt
deleted file mode 100644
index 678a1d7ea..000000000
--- a/misc/requirements/requirements-pyqt-pyinstaller.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# This file is automatically generated by scripts/dev/recompile_requirements.py
-
-PyQt5==5.15.3
-PyQt5-Qt==5.15.2
-PyQt5-sip==12.9.0
-PyQtWebEngine==5.15.3
-PyQtWebEngine-Qt==5.15.2
diff --git a/misc/requirements/requirements-pyqt-pyinstaller.txt-raw b/misc/requirements/requirements-pyqt-pyinstaller.txt-raw
deleted file mode 100644
index 89b5644da..000000000
--- a/misc/requirements/requirements-pyqt-pyinstaller.txt-raw
+++ /dev/null
@@ -1,2 +0,0 @@
-PyQt5==5.15.3
-PyQtWebEngine==5.15.3
diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt
index 75ef27bf4..3953d27b3 100644
--- a/misc/requirements/requirements-pyqt.txt
+++ b/misc/requirements/requirements-pyqt.txt
@@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-PyQt5==5.15.4
+PyQt5==5.15.6
PyQt5-Qt5==5.15.2
PyQt5-sip==12.9.0
-PyQtWebEngine==5.15.4
+PyQtWebEngine==5.15.5
PyQtWebEngine-Qt5==5.15.2
diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt
index 82a00016c..a76402053 100644
--- a/misc/requirements/requirements-pyroma.txt
+++ b/misc/requirements/requirements-pyroma.txt
@@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
certifi==2021.10.8
-charset-normalizer==2.0.6
-docutils==0.17.1
-idna==3.2
+charset-normalizer==2.0.8
+docutils==0.18.1
+idna==3.3
Pygments==2.10.0
pyroma==3.2
requests==2.26.0
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index fb01ec30c..b7f013853 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -3,19 +3,19 @@
alabaster==0.7.12
Babel==2.9.1
certifi==2021.10.8
-charset-normalizer==2.0.6
+charset-normalizer==2.0.8
docutils==0.17.1
-idna==3.2
-imagesize==1.2.0
-Jinja2==3.0.2
+idna==3.3
+imagesize==1.3.0
+Jinja2==3.0.3
MarkupSafe==2.0.1
-packaging==21.0
+packaging==21.3
Pygments==2.10.0
-pyparsing==2.4.7
+pyparsing==3.0.6
pytz==2021.3
requests==2.26.0
-snowballstemmer==2.1.0
-Sphinx==4.2.0
+snowballstemmer==2.2.0
+Sphinx==4.3.1
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
diff --git a/misc/requirements/requirements-tests-bleeding.txt b/misc/requirements/requirements-tests-bleeding.txt
index 49911c48d..d2a7fcfb6 100644
--- a/misc/requirements/requirements-tests-bleeding.txt
+++ b/misc/requirements/requirements-tests-bleeding.txt
@@ -9,7 +9,7 @@ git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-py
git+https://github.com/pytest-dev/pytest.git
# Problematic: https://github.com/pytest-dev/pytest-bdd/issues/447
# git+https://github.com/pytest-dev/pytest-bdd.git
-pytest-bdd
+pytest-bdd<5
git+https://github.com/ionelmc/pytest-benchmark.git
git+https://github.com/pytest-dev/pytest-instafail.git
git+https://github.com/pytest-dev/pytest-mock.git
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 83379d700..b15a23c08 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -3,36 +3,36 @@
attrs==21.2.0
beautifulsoup4==4.10.0
certifi==2021.10.8
-charset-normalizer==2.0.6
+charset-normalizer==2.0.8
cheroot==8.5.2
click==8.0.3
-coverage==6.0.1
+coverage==6.2
EasyProcess==0.3
execnet==1.9.0
-filelock==3.3.0
+filelock==3.4.0
Flask==2.0.2
glob2==0.7
hunter==3.3.8
-hypothesis==6.23.2
+hypothesis==6.28.1
icdiff==2.0.4
-idna==3.2
+idna==3.3
iniconfig==1.1.1
itsdangerous==2.0.1
-jaraco.functools==3.3.0
-# Jinja2==3.0.2
-Mako==1.1.5
+jaraco.functools==3.4.0
+# Jinja2==3.0.3
+Mako==1.1.6
manhole==1.8.0
# MarkupSafe==2.0.1
-more-itertools==8.10.0
-packaging==21.0
+more-itertools==8.12.0
+packaging==21.3
parse==1.19.0
parse-type==0.5.2
pluggy==1.0.0
pprintpp==0.4.0
-py==1.10.0
+py==1.11.0
py-cpuinfo==8.0.0
Pygments==2.10.0
-pyparsing==2.4.7
+pyparsing==3.0.6
pytest==6.2.5
pytest-bdd==4.1.0
pytest-benchmark==3.4.1
@@ -51,10 +51,10 @@ requests==2.26.0
requests-file==1.5.1
six==1.16.0
sortedcontainers==2.4.0
-soupsieve==2.2.1
+soupsieve==2.3.1
tldextract==3.1.2
toml==0.10.2
-tomli==1.2.1
+tomli==1.2.2
urllib3==1.26.7
vulture==2.3
Werkzeug==2.0.2
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index ab580ac4b..5586a86ef 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -4,7 +4,8 @@ coverage
Flask
hypothesis
pytest
-pytest-bdd
+# https://github.com/pytest-dev/pytest-bdd/issues/447
+pytest-bdd<5
pytest-benchmark
pytest-instafail
pytest-mock
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 4c1cfbe27..a2a57808b 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -1,17 +1,17 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-backports.entry-points-selectable==1.1.0
+backports.entry-points-selectable==1.1.1
distlib==0.3.3
-filelock==3.3.0
-packaging==21.0
-pip==21.2.4
+filelock==3.4.0
+packaging==21.3
+pip==21.3.1
platformdirs==2.4.0
pluggy==1.0.0
-py==1.10.0
-pyparsing==2.4.7
-setuptools==58.2.0
+py==1.11.0
+pyparsing==3.0.6
+setuptools==59.4.0
six==1.16.0
toml==0.10.2
tox==3.24.4
-virtualenv==20.8.1
+virtualenv==20.10.0
wheel==0.37.0
diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt
index 897184c74..12553f2b2 100644
--- a/misc/requirements/requirements-yamllint.txt
+++ b/misc/requirements/requirements-yamllint.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pathspec==0.9.0
-PyYAML==5.4.1
+PyYAML==6.0
yamllint==1.26.3
diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill
index c46253d41..3ea8fd9f6 100755
--- a/misc/userscripts/password_fill
+++ b/misc/userscripts/password_fill
@@ -241,7 +241,7 @@ pass_backend() {
if $GPG "${GPG_OPTS[@]}" -d "$passfile" \
| grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null
then
- passfile="${passfile#$PREFIX}"
+ passfile="${passfile#"$PREFIX"}"
passfile="${passfile#/}"
files+=( "${passfile%.gpg}" )
fi
@@ -250,7 +250,7 @@ pass_backend() {
if ((match_filename)) ; then
# add entries with matching filepath
while read -r passfile ; do
- passfile="${passfile#$PREFIX}"
+ passfile="${passfile#"$PREFIX"}"
passfile="${passfile#/}"
files+=( "${passfile%.gpg}" )
done < <(find -L "$PREFIX" -iname '*.gpg' | grep "$url")
@@ -267,7 +267,7 @@ pass_backend() {
else
if [[ $line =~ $user_pattern ]] ; then
# remove the matching prefix "user: " from the beginning of the line
- username=${line#${BASH_REMATCH[0]}}
+ username=${line#"${BASH_REMATCH[0]}"}
break
fi
fi
diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py
index 29a8e4836..c05215792 100644
--- a/qutebrowser/__init__.py
+++ b/qutebrowser/__init__.py
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2021 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
-__version__ = "2.3.1"
+__version__ = "2.4.0"
__version_info__ = tuple(int(part) for part in __version__.split('.'))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index f3438aaa8..796bb2eb3 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -451,7 +451,7 @@ class CommandDispatcher:
self._open(tab.url(), tab=True)
if not keep:
- tabbed_browser.close_tab(tab, add_undo=False)
+ tabbed_browser.close_tab(tab, add_undo=False, transfer=True)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('win_id', completion=miscmodels.window)
@@ -500,7 +500,8 @@ class CommandDispatcher:
tabbed_browser.tabopen(self._current_url())
if not keep:
self._tabbed_browser.close_tab(self._current_widget(),
- add_undo=False)
+ add_undo=False,
+ transfer=True)
def _back_forward(self, tab, bg, window, count, forward, index=None):
"""Helper function for :back/:forward."""
@@ -1004,11 +1005,10 @@ class CommandDispatcher:
raise cmdutils.CommandError("There's no tab with index {}!".format(
index))
- @cmdutils.register(instance='command-dispatcher', scope='window')
- @cmdutils.argument('index', choices=['+', '-'])
- @cmdutils.argument('count', value=cmdutils.Value.count)
- def tab_move(self, index: Union[str, int] = None,
- count: int = None) -> None:
+ @cmdutils.register(instance="command-dispatcher", scope="window")
+ @cmdutils.argument("index", choices=["+", "-", "start", "end"])
+ @cmdutils.argument("count", value=cmdutils.Value.count)
+ def tab_move(self, index: Union[str, int] = None, count: int = None) -> None:
"""Move the current tab according to the argument and [count].
If neither is given, move it to the first position.
@@ -1017,24 +1017,29 @@ class CommandDispatcher:
index: `+` or `-` to move relative to the current tab by
count, or a default of 1 space.
A tab index to move to that index.
+ `start` and `end` to move to the start and the end.
count: If moving relatively: Offset.
If moving absolutely: New position (default: 0). This
overrides the index argument, if given.
"""
- if index in ['+', '-']:
+ if index in ["+", "-"]:
# relative moving
new_idx = self._current_index()
delta = 1 if count is None else count
- if index == '-':
+ if index == "-":
new_idx -= delta
- elif index == '+': # pragma: no branch
+ elif index == "+": # pragma: no branch
new_idx += delta
if config.val.tabs.wrap:
new_idx %= self._count()
else:
# absolute moving
- if count is not None:
+ if index == "start":
+ new_idx = 0
+ elif index == "end":
+ new_idx = self._count() - 1
+ elif count is not None:
new_idx = count - 1
elif index is not None:
assert isinstance(index, int)
diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py
index e40b3e736..f8e1a59b1 100644
--- a/qutebrowser/browser/webengine/notification.py
+++ b/qutebrowser/browser/webengine/notification.py
@@ -715,6 +715,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
# https://github.com/KDE/plasma-workspace/blob/v5.21.4/libnotificationmanager/server_p.cpp#L227-L237
# Created too many similar notifications in quick succession
"org.freedesktop.Notifications.Error.ExcessNotificationGeneration",
+
+ # From https://crashes.qutebrowser.org/view/b8c9838a - probably when
+ # notification daemon crashes?
+ "org.freedesktop.DBus.Error.Spawn.ChildSignaled",
}
def __init__(self, parent: QObject = None) -> None:
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index ace23d14a..926ccf133 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -200,6 +200,14 @@ class WebEngineSearch(browsertab.AbstractSearch):
def _empty_flags(self):
return QWebEnginePage.FindFlags(0) # type: ignore[call-overload]
+ def _args_to_flags(self, reverse, ignore_case):
+ flags = self._empty_flags()
+ if self._is_case_sensitive(ignore_case):
+ flags |= QWebEnginePage.FindCaseSensitively
+ if reverse:
+ flags |= QWebEnginePage.FindBackward
+ return flags
+
def connect_signals(self):
self._wrap_handler.connect_signal(self._widget.page())
@@ -246,17 +254,14 @@ class WebEngineSearch(browsertab.AbstractSearch):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
- " for {}".format(text))
+ " for {}, but resetting flags".format(text))
+ self._flags = self._args_to_flags(reverse, ignore_case)
return
self.text = text
- self._flags = self._empty_flags()
+ self._flags = self._args_to_flags(reverse, ignore_case)
self._wrap_handler.reset_match_data()
self._wrap_handler.flag_wrap = wrap
- if self._is_case_sensitive(ignore_case):
- self._flags |= QWebEnginePage.FindCaseSensitively
- if reverse:
- self._flags |= QWebEnginePage.FindBackward
self._find(text, self._flags, result_cb, 'search')
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index df3491ec2..7a41b995c 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -108,6 +108,16 @@ class WebKitSearch(browsertab.AbstractSearch):
def _empty_flags(self):
return QWebPage.FindFlags(0) # type: ignore[call-overload]
+ def _args_to_flags(self, reverse, ignore_case, wrap):
+ flags = self._empty_flags()
+ if self._is_case_sensitive(ignore_case):
+ flags |= QWebPage.FindCaseSensitively
+ if reverse:
+ flags |= QWebPage.FindBackward
+ if wrap:
+ flags |= QWebPage.FindWrapsAroundDocument
+ return flags
+
def _call_cb(self, callback, found, text, flags, caller):
"""Call the given callback if it's non-None.
@@ -150,7 +160,8 @@ class WebKitSearch(browsertab.AbstractSearch):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
- " for {}".format(text))
+ " for {}, but resetting flags".format(text))
+ self._flags = self._args_to_flags(reverse, ignore_case, wrap)
return
# Clear old search results, this is done automatically on QtWebEngine.
@@ -158,13 +169,7 @@ class WebKitSearch(browsertab.AbstractSearch):
self.text = text
self.search_displayed = True
- self._flags = self._empty_flags()
- if self._is_case_sensitive(ignore_case):
- self._flags |= QWebPage.FindCaseSensitively
- if reverse:
- self._flags |= QWebPage.FindBackward
- if wrap:
- self._flags |= QWebPage.FindWrapsAroundDocument
+ self._flags = self._args_to_flags(reverse, ignore_case, wrap)
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
found = self._widget.findText(text, self._flags)
diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py
index 120806bfe..8eaae045f 100644
--- a/qutebrowser/components/misccommands.py
+++ b/qutebrowser/components/misccommands.py
@@ -183,7 +183,10 @@ def screenshot(
raise cmdutils.CommandError(
f"File {filename} already exists (use --force to overwrite)")
- qrect = None if rect is None else utils.parse_rect(rect)
+ try:
+ qrect = None if rect is None else utils.parse_rect(rect)
+ except ValueError as e:
+ raise cmdutils.CommandError(str(e))
pic = tab.grab_pixmap(qrect)
if pic is None:
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 8b924f9db..cd95124db 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -679,14 +679,14 @@ content.headers.user_agent:
# Vim-protip: Place your cursor below this comment and run
# :r!python scripts/dev/ua_fetch.py
- - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
- like Gecko) Chrome/90.0.4430.93 Safari/537.36"
- - Chrome 90 Win10
+ like Gecko) Chrome/92.0.4515.131 Safari/537.36"
+ - Chrome 92 Win10
- - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
- (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"
- - Chrome 90 macOS
+ (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36"
+ - Chrome 92 macOS
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
- Gecko) Chrome/90.0.4430.93 Safari/537.36"
- - Chrome 90 Linux
+ Gecko) Chrome/92.0.4515.131 Safari/537.36"
+ - Chrome 92 Linux
supports_pattern: true
desc: |
User agent to send.
@@ -1794,6 +1794,17 @@ input.media_keys:
On Linux, disabling this also disables Chromium's MPRIS integration.
+input.match_counts:
+ default: true
+ type: Bool
+ desc: >-
+ Interpret number prefixes as counts for bindings.
+
+ This enables for vi-like bindings that can be prefixed with a number to
+ indicate a count.
+ Disabling it allows for emacs-like bindings where number keys are passed
+ through (according to `input.forward_unbound_keys`) instead.
+
## keyhint
keyhint.blacklist:
diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py
index 7ae45023b..c7b619b3e 100644
--- a/qutebrowser/extensions/loader.py
+++ b/qutebrowser/extensions/loader.py
@@ -21,12 +21,11 @@
import pkgutil
import types
-import sys
import pathlib
import importlib
import argparse
import dataclasses
-from typing import Callable, Iterator, List, Optional, Set, Tuple
+from typing import Callable, Iterator, List, Optional, Tuple
from PyQt5.QtCore import pyqtSlot
@@ -95,18 +94,6 @@ 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 _on_walk_error(name: str) -> None:
- raise ImportError("Failed to import {}".format(name))
-
-
-def _walk_normal() -> Iterator[ExtensionInfo]:
- """Walk extensions when not using PyInstaller."""
for _finder, name, ispkg in pkgutil.walk_packages(
# Only packages have a __path__ attribute,
# but we're sure this is one.
@@ -123,23 +110,6 @@ def _walk_normal() -> 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 # type: ignore[union-attr]
- 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()),
@@ -190,3 +160,7 @@ def _on_config_changed(changed_name: str) -> None:
def init() -> None:
config.instance.changed.connect(_on_config_changed)
+
+
+def _on_walk_error(name: str) -> None:
+ raise ImportError("Failed to import {}".format(name))
diff --git a/qutebrowser/html/settings.html b/qutebrowser/html/settings.html
index 44824eeac..dfbc5c168 100644
--- a/qutebrowser/html/settings.html
+++ b/qutebrowser/html/settings.html
@@ -13,22 +13,112 @@ var cset = function(option, value) {
{% endblock %}
{% block style %}
-table { border: 1px solid grey; border-collapse: collapse; }
-pre { margin: 2px; }
-th, td { border: 1px solid grey; padding: 0px 5px; }
-th { background: lightgrey; }
-th pre { color: grey; text-align: left; }
-input { width: 98%; }
-.setting { width: 75%; }
-.value { width: 25%; text-align: center; }
-.noscript, .noscript-text { color:red; }
-.noscript-text { margin-bottom: 5cm; }
-.option_description { margin: .5ex 0; color: grey; font-size: 80%; font-style: italic; white-space: pre-line; }
+table {
+ border-spacing: 10px;
+}
+
+tbody tr:nth-child(odd) {
+ background: #eaf4fb;
+}
+
+pre {
+ margin: 2px;
+}
+
+th {
+ padding: 10px;
+ border-radius: 5px;
+ background: #a6dfff;
+ text-align: left;
+ font-weight: normal;
+ font-size: 1.5rem;
+ color: #084c88;
+}
+
+td {
+ padding: 5px 5px;
+}
+
+th pre {
+ color: grey;
+ text-align: left;
+}
+
+input {
+ padding: 8px;
+ width: 98%;
+ box-sizing: border-box;
+ border-radius: 4px;
+ border: 1px solid #01cdd0;
+ font-size: 0.9rem;
+ font-family: DejaVu, serif;
+}
+
+input:focus {
+ outline: none;
+ border: 2px solid #7a589ea6;
+}
+
+input[type="radio"] {
+ position: absolute; /* Positions the radio button relative to the edges of its containing element */
+ -webkit-appearance: none; /* Removes its native styling */
+ width: min-content;
+ margin: 0;
+ border: none;
+}
+
+label {
+ cursor: pointer;
+ margin-bottom: 2px;
+ padding: 5px 10px;
+ border-radius: 5px;
+ background-color: #dddddd;
+ color: #666666;
+}
+
+input[type="radio"]:checked + label {
+ background-color: #a6dfff;
+ color: #084c88;
+}
+
+.setting {
+ width: 60%;
+}
+
+.value {
+ width: 25%;
+ text-align: center;
+}
+
+.valid-value {
+ text-align: center;
+}
+
+.noscript, .noscript-text {
+ color: red;
+}
+
+.noscript-text {
+ margin-bottom: 5cm;
+}
+
+.option-description {
+ margin: .5ex 0;
+ color: #635d5dcf;
+ font-size: 80%;
+ font-style: italic;
+ white-space: pre-line;
+}
+
+.radio-button {
+ position: relative; /* The absolutely positioned element inside this tag (the radio button) gets positioned relative to it. */
+ display: inline-flex;
+ margin: 3px 1px;
+}
{% endblock %}
{% block content %}
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
-<header><h1>{{ title }}</h1></header>
<table>
<tr>
<th>Setting</th>
@@ -37,18 +127,36 @@ input { width: 98%; }
{% for option in configdata.DATA.values()|sort(attribute='name') if not option.no_autoconfig %}
<tr>
<!-- FIXME: convert to string properly -->
- <td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})
+ <td class="setting">{{ option.name }}
{% if option.description %}
- <p class="option_description">{{ option.description|e }}</p>
+ <p class="option-description">{{ option.description|e }}</p>
{% endif %}
</td>
- <td class="value">
- <input type="text"
- id="input-{{ option.name }}"
- onblur="cset('{{ option.name }}', this.value)"
- value="{{ confget(option.name) }}">
- </input>
- </td>
+ {% if option.typ.valid_values is not none %}
+ <td class="valid-value">
+ {% for value in option.typ.valid_values.values %}
+ <div class="radio-button">
+ <input type="radio" id="input-{{ option.name }}-{{ loop.index0 }}"
+ name="{{ option.name }}" value="{{ value }}"
+ onclick="cset('{{ option.name }}', this.value)"
+ {% if confget(option.name) == value %}
+ checked
+ {% endif %}>
+ <label for="input-{{ option.name }}-{{ loop.index0 }}">
+ {{ value }}
+ </label>
+ </div>
+ {% endfor %}
+ </td>
+ {% else %}
+ <td class="value">
+ <input type="text"
+ id="input-{{ option.name }}"
+ onblur="cset('{{ option.name }}', this.value)"
+ value="{{ confget(option.name) }}">
+ </input>
+ </td>
+ {% endif %}
</tr>
{% endfor %}
</table>
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
index 7e688dab1..4db1d5d76 100644
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -254,6 +254,9 @@ class BaseKeyParser(QObject):
def _match_count(self, sequence: keyutils.KeySequence,
dry_run: bool) -> bool:
"""Try to match a key as count."""
+ if not config.val.input.match_counts:
+ return False
+
txt = str(sequence[-1]) # To account for sequences changed above.
if (txt in string.digits and self._supports_count and
not (not self._count and txt == '0')):
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index e081284ee..8c6ac2424 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -406,15 +406,16 @@ class TabbedBrowser(QWidget):
else:
yes_action()
- def close_tab(self, tab, *, add_undo=True, new_undo=True):
+ def close_tab(self, tab, *, add_undo=True, new_undo=True, transfer=False):
"""Close a tab.
Args:
tab: The QWebView to be closed.
add_undo: Whether the tab close can be undone.
new_undo: Whether the undo entry should be a new item in the stack.
+ transfer: Whether the tab is closing because it is moving to a new window.
"""
- if config.val.tabs.tabs_are_windows:
+ if config.val.tabs.tabs_are_windows or transfer:
last_close = 'close'
else:
last_close = config.val.tabs.last_close
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index c4ff0bb85..f27b7acfe 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -111,17 +111,23 @@ def init_faulthandler(fileobj=sys.__stderr__):
Args:
fobj: An opened file object to write the traceback to.
"""
- if fileobj is None:
+ try:
+ faulthandler.enable(fileobj)
+ except (RuntimeError, AttributeError):
# When run with pythonw.exe, sys.__stderr__ can be None:
# https://docs.python.org/3/library/sys.html#sys.__stderr__
- # If we'd enable faulthandler in that case, we just get a weird
- # exception, so we don't enable faulthandler if we have no stdout.
+ #
+ # With PyInstaller, it can be a NullWriter raising AttributeError on
+ # fileno: https://github.com/pyinstaller/pyinstaller/issues/4481
#
# Later when we have our data dir available we re-enable faulthandler
# to write to a file so we can display a crash to the user at the next
# start.
+ #
+ # Note that we don't have any logging initialized yet at this point, so
+ # this is a silent error.
return
- faulthandler.enable(fileobj)
+
if (hasattr(faulthandler, 'register') and hasattr(signal, 'SIGUSR1') and
sys.stderr is not None):
# If available, we also want a traceback on SIGUSR1.
diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py
index e5ccd1b8b..c93fad09b 100644
--- a/qutebrowser/misc/guiprocess.py
+++ b/qutebrowser/misc/guiprocess.py
@@ -27,7 +27,7 @@ from typing import Mapping, Sequence, Dict, Optional
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess,
QProcessEnvironment, QByteArray, QUrl, Qt)
-from qutebrowser.utils import message, log, utils, usertypes
+from qutebrowser.utils import message, log, utils, usertypes, version
from qutebrowser.api import cmdutils, apitypes
from qutebrowser.completion.models import miscmodels
@@ -273,7 +273,9 @@ class GUIProcess(QObject):
known_errors = ['No such file or directory', 'Permission denied']
if (': ' in error_string and # pragma: no branch
error_string.split(': ', maxsplit=1)[1] in known_errors):
- msg += f'\n(Hint: Make sure {self.cmd!r} exists and is executable)'
+ msg += f'\nHint: Make sure {self.cmd!r} exists and is executable'
+ if version.is_flatpak():
+ msg += ' inside the Flatpak container'
message.error(msg)
diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py
index d0819f832..c576c4a06 100644
--- a/qutebrowser/qutebrowser.py
+++ b/qutebrowser/qutebrowser.py
@@ -87,6 +87,11 @@ def get_argparser():
help="Set the base name of the desktop entry for this "
"application. Used to set the app_id under Wayland. See "
"https://doc.qt.io/qt-5/qguiapplication.html#desktopFileName-prop")
+ parser.add_argument('--untrusted-args',
+ action='store_true',
+ help="Mark all following arguments as untrusted, which "
+ "enforces that they are URLs/search terms (and not flags or "
+ "commands)")
parser.add_argument('--json-args', help=argparse.SUPPRESS)
parser.add_argument('--temp-basedir-restarted',
@@ -207,7 +212,27 @@ def _unpack_json_args(args):
return argparse.Namespace(**new_args)
+def _validate_untrusted_args(argv):
+ # NOTE: Do not use f-strings here, as this should run with older Python
+ # versions (so that a proper error can be displayed)
+ try:
+ untrusted_idx = argv.index('--untrusted-args')
+ except ValueError:
+ return
+
+ rest = argv[untrusted_idx + 1:]
+ if len(rest) > 1:
+ sys.exit(
+ "Found multiple arguments ({}) after --untrusted-args, "
+ "aborting.".format(' '.join(rest)))
+
+ for arg in rest:
+ if arg.startswith(('-', ':')):
+ sys.exit("Found {} after --untrusted-args, aborting.".format(arg))
+
+
def main():
+ _validate_untrusted_args(sys.argv)
parser = get_argparser()
argv = sys.argv[1:]
args = parser.parse_args(argv)
diff --git a/qutebrowser/utils/resources.py b/qutebrowser/utils/resources.py
index ff5ec9d9a..f561d6747 100644
--- a/qutebrowser/utils/resources.py
+++ b/qutebrowser/utils/resources.py
@@ -82,7 +82,7 @@ def _glob(
else: # zipfile.Path or importlib_resources compat object
# Unfortunately, we can't tell mypy about resource_path being of type
# Union[pathlib.Path, zipfile.Path] because we set "python_version = 3.6" in
- # .mypy.ini, but the zipfiel stubs (correctly) only declare zipfile.Path with
+ # .mypy.ini, but the zipfile stubs (correctly) only declare zipfile.Path with
# Python 3.8...
assert glob_path.is_dir(), glob_path # type: ignore[unreachable]
for subpath in glob_path.iterdir():
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index a56769255..f42515c5c 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -669,11 +669,12 @@ def yaml_load(f: Union[str, IO[str]]) -> Any:
r"of from 'collections\.abc' is deprecated.*"):
try:
data = yaml.load(f, Loader=YamlLoader)
- except ValueError as e:
- if str(e).startswith('could not convert string to float'):
+ except ValueError as e: # pragma: no cover
+ pyyaml_error = 'could not convert string to float'
+ if str(e).startswith(pyyaml_error):
# WORKAROUND for https://github.com/yaml/pyyaml/issues/168
raise yaml.YAMLError(e)
- raise # pragma: no cover
+ raise
end = datetime.datetime.now()
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 8cd244fca..3beb6fb83 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -773,8 +773,6 @@ def _backend() -> str:
if objects.backend == usertypes.Backend.QtWebKit:
return 'new QtWebKit (WebKit {})'.format(qWebKitVersion())
elif objects.backend == usertypes.Backend.QtWebEngine:
- webengine = usertypes.Backend.QtWebEngine
- assert objects.backend == webengine, objects.backend
return str(qtwebengine_versions(
avoid_init='avoid-chromium-init' in objects.debug_flags))
raise utils.Unreachable(objects.backend)
diff --git a/requirements.txt b/requirements.txt
index a158bdde6..e088ca805 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,11 +3,11 @@
adblock==0.5.0
colorama==0.4.4
dataclasses==0.6 ; python_version<"3.7"
-importlib-metadata==4.8.1 ; python_version<"3.8"
-importlib-resources==5.2.2 ; python_version<"3.9"
-Jinja2==3.0.2
+importlib-metadata==4.8.2 ; python_version<"3.8"
+importlib-resources==5.4.0 ; python_version<"3.9"
+Jinja2==3.0.3
MarkupSafe==2.0.1
Pygments==2.10.0
-PyYAML==5.4.1
-typing-extensions==3.10.0.2
+PyYAML==6.0
+typing_extensions==4.0.0 ; python_version<"3.8"
zipp==3.6.0
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index a1c6646eb..4961cbdc8 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -227,7 +227,7 @@ def patch_mac_app():
# Replace some duplicate files by symlinks
framework_path = os.path.join(app_path, 'Contents', 'MacOS', 'PyQt5',
- 'Qt', 'lib', 'QtWebEngineCore.framework')
+ 'Qt5', 'lib', 'QtWebEngineCore.framework')
core_lib = os.path.join(framework_path, 'Versions', '5', 'QtWebEngineCore')
os.remove(core_lib)
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index a4cd81ad4..1b9759eb8 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -98,7 +98,6 @@ CHANGELOG_URLS = {
'pep8-naming': 'https://github.com/PyCQA/pep8-naming/blob/master/CHANGELOG.rst',
'pycodestyle': 'https://github.com/PyCQA/pycodestyle/blob/master/CHANGES.txt',
'pyflakes': 'https://github.com/PyCQA/pyflakes/blob/master/NEWS.rst',
- 'cached-property': 'https://github.com/pydanny/cached-property/blob/master/HISTORY.md',
'cffi': 'https://github.com/python-cffi/release-doc/blob/master/doc/source/whatsnew.rst',
'astroid': 'https://github.com/PyCQA/astroid/blob/2.4/ChangeLog',
'pytest-instafail': 'https://github.com/pytest-dev/pytest-instafail/blob/master/CHANGES.rst',
@@ -134,7 +133,7 @@ CHANGELOG_URLS = {
'six': 'https://github.com/benjaminp/six/blob/master/CHANGES',
'altgraph': 'https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst',
'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst',
- 'lxml': 'https://lxml.de/index.html#old-versions',
+ 'lxml': 'https://github.com/lxml/lxml/blob/master/CHANGES.txt',
'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master',
'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst',
'pep517': 'https://github.com/pypa/pep517/blob/master/doc/changelog.rst',
@@ -142,10 +141,8 @@ CHANGELOG_URLS = {
'toml': 'https://github.com/uiri/toml/releases',
'tomli': 'https://github.com/hukkin/tomli/blob/master/CHANGELOG.md',
'PyQt5': 'https://www.riverbankcomputing.com/news',
- 'PyQt5-Qt': 'https://www.riverbankcomputing.com/news',
'PyQt5-Qt5': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine': 'https://www.riverbankcomputing.com/news',
- 'PyQtWebEngine-Qt': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine-Qt5': 'https://www.riverbankcomputing.com/news',
'PyQt-builder': 'https://www.riverbankcomputing.com/news',
'PyQt5-sip': 'https://www.riverbankcomputing.com/news',
@@ -158,11 +155,11 @@ CHANGELOG_URLS = {
'cheroot': 'https://cheroot.cherrypy.org/en/latest/history.html',
'certifi': 'https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport',
'chardet': 'https://github.com/chardet/chardet/releases',
- 'charset-normalizer': 'https://github.com/Ousret/charset_normalizer/commits/master',
+ 'charset-normalizer': 'https://github.com/Ousret/charset_normalizer/blob/master/CHANGELOG.md',
'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst',
'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md',
'backports.entry-points-selectable': 'https://github.com/jaraco/backports.entry_points_selectable/blob/main/CHANGES.rst',
- 'typing-extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
+ 'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
'diff-cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG',
'pytest-icdiff': 'https://github.com/hjwp/pytest-icdiff/blob/master/HISTORY.rst',
'icdiff': 'https://github.com/jeffkaufman/icdiff/blob/master/ChangeLog',
@@ -171,7 +168,7 @@ CHANGELOG_URLS = {
'check-manifest': 'https://github.com/mgedmin/check-manifest/blob/master/CHANGES.rst',
'yamllint': 'https://github.com/adrienverge/yamllint/blob/master/CHANGELOG.rst',
'pathspec': 'https://github.com/cpburnz/python-path-specification/blob/master/CHANGES.rst',
- 'filelock': 'https://github.com/tox-dev/py-filelock/commits/main',
+ 'filelock': 'https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst',
'github3.py': 'https://github3py.readthedocs.io/en/master/release-notes/index.html',
'manhole': 'https://github.com/ionelmc/python-manhole/blob/master/CHANGELOG.rst',
'pycparser': 'https://github.com/eliben/pycparser/blob/master/CHANGES',
diff --git a/tests/conftest.py b/tests/conftest.py
index 40631af34..26cc04345 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -214,20 +214,74 @@ def pytest_addoption(parser):
help="Delay between qutebrowser commands.")
parser.addoption('--qute-profile-subprocs', action='store_true',
default=False, help="Run cProfile for subprocesses.")
- parser.addoption('--qute-bdd-webengine', action='store_true',
- help='Use QtWebEngine for BDD tests')
+ parser.addoption('--qute-backend', action='store',
+ choices=['webkit', 'webengine'], help='Set backend for BDD tests')
def pytest_configure(config):
- webengine_arg = config.getoption('--qute-bdd-webengine')
- webengine_env = os.environ.get('QUTE_BDD_WEBENGINE', 'false')
- config.webengine = webengine_arg or webengine_env == 'true'
- # Fail early if QtWebEngine is not available
- if config.webengine:
- import PyQt5.QtWebEngineWidgets
+ backend = _select_backend(config)
+ config.webengine = backend == 'webengine'
+
earlyinit.configure_pyqt()
+def _select_backend(config):
+ """Select the backend for running tests.
+
+ The backend is auto-selected in the following manner:
+ 1. Use QtWebKit if available
+ 2. Otherwise use QtWebEngine as a fallback
+
+ Auto-selection is overridden by either passing a backend via
+ `--qute-backend=<backend>` or setting the environment variable
+ `QUTE_TESTS_BACKEND=<backend>`.
+
+ Args:
+ config: pytest config
+
+ Raises:
+ ImportError if the selected backend is not available.
+
+ Returns:
+ The selected backend as a string (e.g. 'webkit').
+ """
+ backend_arg = config.getoption('--qute-backend')
+ backend_env = os.environ.get('QUTE_TESTS_BACKEND')
+
+ backend = backend_arg or backend_env or _auto_select_backend()
+
+ # Fail early if selected backend is not available
+ if backend == 'webkit':
+ import PyQt5.QtWebKitWidgets
+ elif backend == 'webengine':
+ import PyQt5.QtWebEngineWidgets
+ else:
+ raise utils.Unreachable(backend)
+
+ return backend
+
+
+def _auto_select_backend():
+ try:
+ # Try to use QtWebKit as the default backend
+ import PyQt5.QtWebKitWidgets
+ return 'webkit'
+ except ImportError:
+ # Try to use QtWebEngine as a fallback and fail early
+ # if that's also not available
+ import PyQt5.QtWebEngineWidgets
+ return 'webengine'
+
+
+def pytest_report_header(config):
+ if config.webengine:
+ backend_version = version.qtwebengine_versions(avoid_init=True)
+ else:
+ backend_version = version.qWebKitVersion()
+
+ return f'backend: {backend_version}'
+
+
@pytest.fixture(scope='session', autouse=True)
def check_display(request):
if utils.is_linux and not os.environ.get('DISPLAY', ''):
diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py
index a4a089cea..16170d460 100644
--- a/tests/end2end/conftest.py
+++ b/tests/end2end/conftest.py
@@ -165,7 +165,7 @@ if not getattr(sys, 'frozen', False):
def pytest_collection_modifyitems(config, items):
- """Apply @qtwebengine_* markers; skip unittests with QUTE_BDD_WEBENGINE."""
+ """Apply @qtwebengine_* markers."""
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884
# (note this isn't actually fixed properly before Qt 5.15)
header_bug_fixed = qtutils.version_check('5.15', compiled=False)
diff --git a/tests/end2end/data/hints/html/README.md b/tests/end2end/data/hints/html/README.md
index 2a6e97c24..5bbaecb4a 100644
--- a/tests/end2end/data/hints/html/README.md
+++ b/tests/end2end/data/hints/html/README.md
@@ -3,3 +3,5 @@ Tests in this directory are automatically picked up by `test_hints` in
They need to contain a special `<!-- target: foo.html -->` comment which
specifies where the hint in it will point to, and will then test that.
+
+With `<!-- target: null -->`, the page is expected to not generate any hints.
diff --git a/tests/end2end/data/hints/invisible.html b/tests/end2end/data/hints/html/invisible.html
index b0bfa9dd9..d382c80fa 100644
--- a/tests/end2end/data/hints/invisible.html
+++ b/tests/end2end/data/hints/html/invisible.html
@@ -1,3 +1,5 @@
+<!-- target: null -->
+
<!DOCTYPE html>
<html>
diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature
index cf35c5356..47153b741 100644
--- a/tests/end2end/features/hints.feature
+++ b/tests/end2end/features/hints.feature
@@ -241,11 +241,6 @@ Feature: Using hints
# The actual check is already done above
Then "No elements found." should not be logged
- Scenario: Hinting invisible elements
- When I open data/hints/invisible.html
- And I run :hint
- Then the error "No elements found." should be shown
-
Scenario: Clicking input with existing text
When I open data/hints/input.html
And I run :click-element id qute-input-existing
diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature
index 5fafd19f0..305b45690 100644
--- a/tests/end2end/features/search.feature
+++ b/tests/end2end/features/search.feature
@@ -71,7 +71,25 @@ Feature: Searching on a page
When I run :search foo
And I wait for "search found foo" in the log
And I run :search foo
- Then "Ignoring duplicate search request for foo" should be logged
+ Then "Ignoring duplicate search request for foo, but resetting flags" should be logged
+
+ Scenario: Reset search direction on duplicate search, forward-to-back
+ When I run :search baz
+ And I wait for "search found baz" in the log
+ And I run :search -r baz
+ And I wait for "Ignoring duplicate search request for baz, but resetting flags" in the log
+ And I run :search-next
+ And I wait for "next_result found baz with flags FindBackward" in the log
+ Then "BAZ" should be found
+
+ Scenario: Reset search direction on duplicate search, back-to-forward
+ When I run :search -r baz
+ And I wait for "search found baz with flags FindBackward" in the log
+ And I run :search baz
+ And I wait for "Ignoring duplicate search request for baz, but resetting flags" in the log
+ And I run :search-next
+ And I wait for "next_result found baz" in the log
+ Then "baz" should be found
## search.ignore_case
diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature
index c9d983755..3715d5765 100644
--- a/tests/end2end/features/tabs.feature
+++ b/tests/end2end/features/tabs.feature
@@ -633,6 +633,27 @@ Feature: Tab management
- data/numbers/1.txt (active)
- data/numbers/3.txt
+ Scenario: :tab-move with absolute position
+ When I open data/numbers/1.txt
+ And I open data/numbers/2.txt in a new tab
+ And I open data/numbers/3.txt in a new tab
+ And I run :tab-focus 1
+ And I run :tab-move end
+ Then the following tabs should be open:
+ - data/numbers/2.txt
+ - data/numbers/3.txt
+ - data/numbers/1.txt (active)
+
+ Scenario: :tab-move with absolute position
+ When I open data/numbers/1.txt
+ And I open data/numbers/2.txt in a new tab
+ And I open data/numbers/3.txt in a new tab
+ And I run :tab-move start
+ Then the following tabs should be open:
+ - data/numbers/3.txt (active)
+ - data/numbers/1.txt
+ - data/numbers/2.txt
+
Scenario: Make sure :tab-move retains metadata
When I open data/title.html
And I open data/hello.txt in a new tab
@@ -1349,6 +1370,25 @@ Feature: Tab management
And I run :tab-take 0/1
Then the error "Can't take tabs when using windows as tabs" should be shown
+ @windows_skip
+ Scenario: Close the last tab of a window when taken by another window
+ Given I have a fresh instance
+ When I open data/numbers/1.txt
+ And I run :tab-only
+ And I open data/numbers/2.txt in a new window
+ And I set tabs.last_close to ignore
+ And I run :tab-take 1/1
+ And I wait until data/numbers/2.txt is loaded
+ Then the session should look like:
+ windows:
+ - tabs:
+ - history:
+ - url: about:blank
+ - url: http://localhost:*/data/numbers/1.txt
+ - active: true
+ history:
+ - url: http://localhost:*/data/numbers/2.txt
+
# :tab-give
@xfail_norun # Needs qutewm
@@ -1406,6 +1446,24 @@ Feature: Tab management
And I run :tab-give 0
Then the error "Can't give tabs when using windows as tabs" should be shown
+ @windows_skip
+ Scenario: Close the last tab of a window when given to another window
+ Given I have a fresh instance
+ When I open data/numbers/1.txt
+ And I run :tab-only
+ And I open data/numbers/2.txt in a new window
+ And I set tabs.last_close to ignore
+ And I run :tab-give 1
+ And I wait until data/numbers/1.txt is loaded
+ Then the session should look like:
+ windows:
+ - tabs:
+ - active: true
+ history:
+ - url: http://localhost:*/data/numbers/2.txt
+ - history:
+ - url: http://localhost:*/data/numbers/1.txt
+
# Other
Scenario: Using :tab-next after closing last tab (#1448)
diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py
index ebb2a7e33..f1cda97fe 100644
--- a/tests/end2end/test_hints_html.py
+++ b/tests/end2end/test_hints_html.py
@@ -40,7 +40,7 @@ def collect_tests():
@dataclasses.dataclass
class ParsedFile:
- target: str
+ target: Optional[str]
qtwebengine_todo: Optional[str]
@@ -107,11 +107,18 @@ def test_hints(test_name, zoom_text_only, zoom_level, find_implementation,
quteproc.set_setting('zoom.text_only', str(zoom_text_only))
quteproc.set_setting('hints.find_implementation', find_implementation)
quteproc.send_cmd(':zoom {}'.format(zoom_level))
+
# follow hint
quteproc.send_cmd(':hint all normal')
- quteproc.wait_for(message='hints: a', category='hints')
- quteproc.send_cmd(':hint-follow a')
- quteproc.wait_for_load_finished('data/' + parsed.target)
+
+ if parsed.target is None:
+ msg = quteproc.wait_for(message='No elements found.', category='message')
+ msg.expected = True
+ else:
+ quteproc.wait_for(message='hints: a', category='hints')
+ quteproc.send_cmd(':hint-follow a')
+ quteproc.wait_for_load_finished('data/' + parsed.target)
+
# reset
quteproc.send_cmd(':zoom 100')
if not request.config.webengine:
diff --git a/tests/unit/extensions/test_loader.py b/tests/unit/extensions/test_loader.py
index feb5dd347..e9b8055aa 100644
--- a/tests/unit/extensions/test_loader.py
+++ b/tests/unit/extensions/test_loader.py
@@ -35,16 +35,10 @@ def test_on_walk_error():
def test_walk_normal():
- names = [info.name for info in loader._walk_normal()]
+ names = [info.name for info in loader.walk_components()]
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/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py
index 30ee36301..84068bf47 100644
--- a/tests/unit/keyinput/test_basekeyparser.py
+++ b/tests/unit/keyinput/test_basekeyparser.py
@@ -346,3 +346,14 @@ def test_clear_keystring_empty(qtbot, keyparser):
keyparser._sequence = keyseq('')
with qtbot.assert_not_emitted(keyparser.keystring_updated):
keyparser.clear_keystring()
+
+
+def test_respect_config_when_matching_counts(keyparser, config_stub):
+ """Don't match counts if disabled in the config."""
+ config_stub.val.input.match_counts = False
+
+ info = keyutils.KeyInfo(Qt.Key_1, Qt.NoModifier)
+ keyparser.handle(info.to_event())
+
+ assert not keyparser._sequence
+ assert not keyparser._count
diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py
index be86bf215..faf2006de 100644
--- a/tests/unit/misc/test_guiprocess.py
+++ b/tests/unit/misc/test_guiprocess.py
@@ -26,7 +26,7 @@ import pytest
from PyQt5.QtCore import QProcess, QUrl
from qutebrowser.misc import guiprocess
-from qutebrowser.utils import usertypes, utils
+from qutebrowser.utils import usertypes, utils, version
from qutebrowser.api import cmdutils
from qutebrowser.qt import sip
@@ -394,8 +394,11 @@ def test_running(qtbot, proc, py_proc):
proc.outcome.was_successful()
-def test_failing_to_start(qtbot, proc, caplog, message_mock):
+@pytest.mark.parametrize('is_flatpak', [True, False])
+def test_failing_to_start(qtbot, proc, caplog, message_mock, monkeypatch, is_flatpak):
"""Test the process failing to start."""
+ monkeypatch.setattr(version, 'is_flatpak', lambda: is_flatpak)
+
with caplog.at_level(logging.ERROR, 'message'):
with qtbot.wait_signal(proc.error, timeout=5000):
proc.start('this_does_not_exist_either', [])
@@ -405,8 +408,11 @@ def test_failing_to_start(qtbot, proc, caplog, message_mock):
"Testprocess 'this_does_not_exist_either' failed to start:")
if not utils.is_windows:
- assert msg.text.endswith(
- "(Hint: Make sure 'this_does_not_exist_either' exists and is executable)")
+ expected_msg = (
+ "Hint: Make sure 'this_does_not_exist_either' exists and is executable")
+ if is_flatpak:
+ expected_msg += ' inside the Flatpak container'
+ assert msg.text.endswith(expected_msg)
assert not proc.outcome.running
assert proc.outcome.status is None
diff --git a/tests/unit/test_qutebrowser.py b/tests/unit/test_qutebrowser.py
index d9275631d..36b4065a1 100644
--- a/tests/unit/test_qutebrowser.py
+++ b/tests/unit/test_qutebrowser.py
@@ -22,6 +22,8 @@
(Mainly commandline flag parsing)
"""
+import re
+
import pytest
from qutebrowser import qutebrowser
@@ -75,3 +77,61 @@ class TestJsonArgs:
# pylint: disable=no-member
assert args.debug
assert not args.temp_basedir
+
+
+class TestValidateUntrustedArgs:
+
+ @pytest.mark.parametrize('args', [
+ [],
+ [':nop'],
+ [':nop', '--untrusted-args'],
+ [':nop', '--debug', '--untrusted-args'],
+ [':nop', '--untrusted-args', 'foo'],
+ ['--debug', '--untrusted-args', 'foo'],
+ ['foo', '--untrusted-args', 'bar'],
+ ])
+ def test_valid(self, args):
+ qutebrowser._validate_untrusted_args(args)
+
+ @pytest.mark.parametrize('args, message', [
+ (
+ ['--untrusted-args', '--debug'],
+ "Found --debug after --untrusted-args, aborting.",
+ ),
+ (
+ ['--untrusted-args', ':nop'],
+ "Found :nop after --untrusted-args, aborting.",
+ ),
+ (
+ ['--debug', '--untrusted-args', '--debug'],
+ "Found --debug after --untrusted-args, aborting.",
+ ),
+ (
+ [':nop', '--untrusted-args', '--debug'],
+ "Found --debug after --untrusted-args, aborting.",
+ ),
+ (
+ [':nop', '--untrusted-args', ':nop'],
+ "Found :nop after --untrusted-args, aborting.",
+ ),
+ (
+ [
+ ':nop',
+ '--untrusted-args',
+ ':nop',
+ '--untrusted-args',
+ 'https://www.example.org',
+ ],
+ (
+ "Found multiple arguments (:nop --untrusted-args "
+ "https://www.example.org) after --untrusted-args, aborting."
+ )
+ ),
+ (
+ ['--untrusted-args', 'okay1', 'okay2'],
+ "Found multiple arguments (okay1 okay2) after --untrusted-args, aborting.",
+ ),
+ ])
+ def test_invalid(self, args, message):
+ with pytest.raises(SystemExit, match=re.escape(message)):
+ qutebrowser._validate_untrusted_args(args)
diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py
index 35ccc94fe..caf52c76d 100644
--- a/tests/unit/utils/test_urlmatch.py
+++ b/tests/unit/utils/test_urlmatch.py
@@ -37,24 +37,30 @@ from PyQt5.QtCore import QUrl
from qutebrowser.utils import urlmatch
+# pylint: disable=line-too-long
+
@pytest.mark.parametrize('pattern, error', [
### Chromium: kMissingSchemeSeparator
## TEST(ExtensionURLPatternTest, ParseInvalid)
# ("http", "No scheme given"),
- ("http:", "Invalid port: Port is empty"),
- ("http:/", "Invalid port: Port is empty"),
- ("about://", "Pattern without path"),
- ("http:/bar", "Invalid port: Port is empty"),
+ pytest.param("http:", "Invalid port: Port is empty", id='scheme-no-slash'),
+ pytest.param("http:/", "Invalid port: Port is empty", id='scheme-single-slash'),
+ pytest.param("about://", "Pattern without path", id='scheme-no-path'),
+ pytest.param(
+ "http:/bar",
+ "Invalid port: Port is empty",
+ id='scheme-single-slash-path',
+ ),
### Chromium: kEmptyHost
## TEST(ExtensionURLPatternTest, ParseInvalid)
- ("http://", "Pattern without host"),
- ("http:///", "Pattern without host"),
- ("http://:1234/", "Pattern without host"),
- ("http://*./", "Pattern without host"),
+ pytest.param("http://", "Pattern without host", id='host-double-slash'),
+ pytest.param("http:///", "Pattern without host", id='host-triple-slash'),
+ pytest.param("http://:1234/", "Pattern without host", id='host-port'),
+ pytest.param("http://*./", "Pattern without host", id='host-pattern'),
## TEST(ExtensionURLPatternTest, IPv6Patterns)
- ("http://[]:8888/*", "Pattern without host"),
+ pytest.param("http://[]:8888/*", "Pattern without host", id='host-ipv6'),
### Chromium: kEmptyPath
## TEST(ExtensionURLPatternTest, ParseInvalid)
@@ -63,53 +69,132 @@ from qutebrowser.utils import urlmatch
### Chromium: kInvalidHost
## TEST(ExtensionURLPatternTest, ParseInvalid)
- ("http://\0www/", "May not contain NUL byte"),
+ pytest.param("http://\0www/", "May not contain NUL byte", id='host-nul'),
## TEST(ExtensionURLPatternTest, IPv6Patterns)
# No closing bracket (`]`).
- ("http://[2607:f8b0:4005:805::200e/*", "Invalid IPv6 URL"),
+ pytest.param(
+ "http://[2607:f8b0:4005:805::200e/*",
+ "Invalid IPv6 URL",
+ id='host-ipv6-no-closing',
+ ),
# Two closing brackets (`]]`).
- pytest.param("http://[2607:f8b0:4005:805::200e]]/*", "Invalid IPv6 URL", marks=pytest.mark.xfail(reason="https://bugs.python.org/issue34360")),
+ pytest.param(
+ "http://[2607:f8b0:4005:805::200e]]/*",
+ "Invalid IPv6 URL",
+ marks=pytest.mark.xfail(reason="https://bugs.python.org/issue34360"),
+ id='host-ipv6-two-closing',
+ ),
# Two open brackets (`[[`).
- ("http://[[2607:f8b0:4005:805::200e]/*", r"""Expected '\]' to match '\[' in hostname; source was "\[2607:f8b0:4005:805::200e"; host = """""),
+ pytest.param(
+ "http://[[2607:f8b0:4005:805::200e]/*",
+ r"""Expected '\]' to match '\[' in hostname; source was "\[2607:f8b0:4005:805::200e"; host = """"",
+ id='host-ipv6-two-open',
+ ),
# Too few colons in the last chunk.
- ("http://[2607:f8b0:4005:805:200e]/*", 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e"; host = ""'),
+ pytest.param(
+ "http://[2607:f8b0:4005:805:200e]/*",
+ 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e"; host = ""',
+ id='host-ipv6-colons',
+ ),
# Non-hex piece.
- ("http://[2607:f8b0:4005:805:200e:12:bogus]/*", 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e:12:bogus"; host = ""'),
+ pytest.param(
+ "http://[2607:f8b0:4005:805:200e:12:bogus]/*",
+ 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e:12:bogus"; host = ""',
+ id='host-ipv6-non-hex',
+ ),
### Chromium: kInvalidHostWildcard
## TEST(ExtensionURLPatternTest, ParseInvalid)
- ("http://*foo/bar", "Invalid host wildcard"),
- ("http://foo.*.bar/baz", "Invalid host wildcard"),
- ("http://fo.*.ba:123/baz", "Invalid host wildcard"),
- ("http://foo.*/bar", "Invalid host wildcard"),
+ pytest.param("http://*foo/bar", "Invalid host wildcard", id='host-wildcard-no-dot'),
+ pytest.param(
+ "http://foo.*.bar/baz",
+ "Invalid host wildcard",
+ id='host-wildcard-middle',
+ ),
+ pytest.param(
+ "http://fo.*.ba:123/baz",
+ "Invalid host wildcard",
+ id='host-wildcard-middle-port',
+ ),
+ pytest.param("http://foo.*/bar", "Invalid host wildcard", id='host-wildcard-end'),
### Chromium: kInvalidPort
## TEST(ExtensionURLPatternTest, Ports)
- ("http://foo:/", "Invalid port: Port is empty"),
- ("http://*.foo:/", "Invalid port: Port is empty"),
- ("http://foo:com/", "Invalid port: .* 'com'"),
- ("http://foo:123456/", "Invalid port: Port out of range 0-65535"),
- ("http://foo:80:80/monkey", "Invalid port: .* '80:80'"),
- ("chrome://foo:1234/bar", "Ports are unsupported with chrome scheme"),
+ pytest.param("http://foo:/", "Invalid port: Port is empty", id='port-empty'),
+ pytest.param(
+ "http://*.foo:/",
+ "Invalid port: Port is empty",
+ id='port-empty-wildcard',
+ ),
+ pytest.param("http://foo:com/", "Invalid port: .* 'com'", id='port-alpha'),
+ pytest.param(
+ "http://foo:123456/",
+ "Invalid port: Port out of range 0-65535",
+ id='port-range',
+ ),
+ pytest.param(
+ "http://foo:80:80/monkey",
+ "Invalid port: .* '80:80'",
+ id='port-double',
+ ),
+ pytest.param(
+ "chrome://foo:1234/bar",
+ "Ports are unsupported with chrome scheme",
+ id='port-chrome',
+ ),
# No port specified, but port separator.
- ("http://[2607:f8b0:4005:805::200e]:/*", "Invalid port: Port is empty"),
+ pytest.param(
+ "http://[2607:f8b0:4005:805::200e]:/*",
+ "Invalid port: Port is empty",
+ id='port-empty-ipv6',
+ ),
### Additional tests
- ("http://[", "Invalid IPv6 URL"),
- ("http://[fc2e::bb88::edac]", 'Invalid IPv6 address; source was "fc2e::bb88::edac"; host = ""'),
- ("http://[fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac"; host = ""'),
- ("http://[fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac"; host = ""'),
- ("http://[127.0.0.1:fc2e::bb88:edac]", r'Invalid IPv6 address; source was "127\.0\.0\.1:fc2e::bb88:edac'),
- ("http://[fc2e::bb88", "Invalid IPv6 URL"),
- ("http://[fc2e:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:bb88:edac"; host = ""'),
- ("http://[fc2e:bb88:edac::z]", 'Invalid IPv6 address; source was "fc2e:bb88:edac::z"; host = ""'),
- ("http://[fc2e:bb88:edac::2]:2a2", "Invalid port: .* '2a2'"),
- ("://", "Missing scheme"),
+ pytest.param("http://[", "Invalid IPv6 URL", id='ipv6-single-open'),
+ pytest.param(
+ "http://[fc2e::bb88::edac]",
+ 'Invalid IPv6 address; source was "fc2e::bb88::edac"; host = ""',
+ id='ipv6-double-double',
+ ),
+ pytest.param(
+ "http://[fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac]",
+ 'Invalid IPv6 address; source was "fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac"; host = ""',
+ id='ipv6-long-double',
+ ),
+ pytest.param(
+ "http://[fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac]",
+ 'Invalid IPv6 address; source was "fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac"; host = ""',
+ id='ipv6-long',
+ ),
+ pytest.param(
+ "http://[127.0.0.1:fc2e::bb88:edac]",
+ r'Invalid IPv6 address; source was "127\.0\.0\.1:fc2e::bb88:edac',
+ id='ipv6-ipv4',
+ ),
+ pytest.param("http://[fc2e::bb88", "Invalid IPv6 URL", id='ipv6-trailing'),
+ pytest.param(
+ "http://[fc2e:bb88:edac]",
+ 'Invalid IPv6 address; source was "fc2e:bb88:edac"; host = ""',
+ id='ipv6-short',
+ ),
+ pytest.param(
+ "http://[fc2e:bb88:edac::z]",
+ 'Invalid IPv6 address; source was "fc2e:bb88:edac::z"; host = ""',
+ id='ipv6-z',
+ ),
+ pytest.param(
+ "http://[fc2e:bb88:edac::2]:2a2",
+ "Invalid port: .* '2a2'",
+ id='ipv6-port',
+ ),
+ pytest.param("://", "Missing scheme", id='scheme-naked'),
])
def test_invalid_patterns(pattern, error):
with pytest.raises(urlmatch.ParseError, match=error):
urlmatch.UrlPattern(pattern)
+# pylint: enable=line-too-long
+
@pytest.mark.parametrize('host', ['.', ' ', ' .', '. ', '. .', '. . .', ' . '])
def test_whitespace_hosts(host):
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 6c57cb3d3..1ffbe3c09 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -484,17 +484,20 @@ class TestGitStrSubprocess:
@needs_git
def test_real_git(self, git_repo):
"""Test with a real git repository."""
- branch_name = subprocess.run(
- ['git', 'config', 'init.defaultBranch'],
- check=False,
- stdout=subprocess.PIPE,
- encoding='utf-8',
- ).stdout.strip()
- if not branch_name:
- branch_name = 'master'
+ def _get_git_setting(name, default):
+ return subprocess.run(
+ ['git', 'config', '--default', default, name],
+ check=True,
+ stdout=subprocess.PIPE,
+ encoding='utf-8',
+ ).stdout.strip()
ret = version._git_str_subprocess(str(git_repo))
- assert ret == f'6e4b65a on {branch_name} (1970-01-01 01:00:00 +0100)'
+ branch_name = _get_git_setting('init.defaultBranch', 'master')
+ abbrev_length = int(_get_git_setting('core.abbrev', '7'))
+ expected_sha = '6e4b65a529c0ab78fb370c1527d5809f7436b8f3'[:abbrev_length]
+
+ assert ret == f'{expected_sha} on {branch_name} (1970-01-01 01:00:00 +0100)'
def test_missing_dir(self, tmp_path):
"""Test with a directory which doesn't exist."""
diff --git a/tox.ini b/tox.ini
index 4be5b8620..f52d7b158 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,7 +13,6 @@ minversion = 3.15
setenv =
PYTEST_QT_API=pyqt5
pyqt{,512,513,514,515,5150}: LINK_PYQT_SKIP=true
- pyqt{,512,513,514,515,5150}: QUTE_BDD_WEBENGINE=true
cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report=
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS DBUS_SESSION_BUS_ADDRESS
basepython =
@@ -42,7 +41,6 @@ commands =
basepython = {env:PYTHON:python3}
setenv =
PYTEST_QT_API=pyqt5
- QUTE_BDD_WEBENGINE=true
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
@@ -160,7 +158,7 @@ passenv = APPDATA HOME PYINSTALLER_DEBUG
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-pyinstaller.txt
- -r{toxinidir}/misc/requirements/requirements-pyqt-pyinstaller.txt
+ -r{toxinidir}/misc/requirements/requirements-pyqt.txt
commands =
{envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec