summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bumpversion.cfg2
-rw-r--r--.flake813
-rw-r--r--.github/CONTRIBUTING.asciidoc6
-rw-r--r--.github/workflows/bleeding.yml4
-rw-r--r--.github/workflows/ci.yml6
-rw-r--r--.gitignore1
-rw-r--r--.mypy.ini1
-rw-r--r--.pylintrc27
-rw-r--r--README.asciidoc28
-rw-r--r--doc/changelog.asciidoc157
-rw-r--r--doc/contributing.asciidoc2
-rw-r--r--doc/help/commands.asciidoc15
-rw-r--r--doc/help/configuring.asciidoc30
-rw-r--r--doc/help/index.asciidoc6
-rw-r--r--doc/help/settings.asciidoc297
-rw-r--r--doc/img/cheatsheet-big.pngbin781120 -> 708315 bytes
-rw-r--r--doc/img/cheatsheet-small.pngbin30252 -> 26611 bytes
-rw-r--r--doc/install.asciidoc4
-rw-r--r--doc/quickstart.asciidoc6
-rw-r--r--doc/qutebrowser.1.asciidoc11
-rw-r--r--doc/userscripts.asciidoc1
-rwxr-xr-xmisc/nsis/install.nsh3
-rw-r--r--misc/org.qutebrowser.qutebrowser.appdata.xml2
-rw-r--r--misc/org.qutebrowser.qutebrowser.desktop2
-rw-r--r--misc/qutebrowser.rcc18
-rw-r--r--misc/requirements/requirements-check-manifest.txt12
-rw-r--r--misc/requirements/requirements-dev.txt57
-rw-r--r--misc/requirements/requirements-dev.txt-raw2
-rw-r--r--misc/requirements/requirements-flake8.txt20
-rw-r--r--misc/requirements/requirements-flake8.txt-raw1
-rw-r--r--misc/requirements/requirements-mypy.txt30
-rw-r--r--misc/requirements/requirements-pip.txt8
-rw-r--r--misc/requirements/requirements-pyinstaller.txt6
-rw-r--r--misc/requirements/requirements-pylint.txt46
-rw-r--r--misc/requirements/requirements-pylint.txt-raw8
-rw-r--r--misc/requirements/requirements-pyqt-5.12.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.13.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.14.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.15.0.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.15.txt6
-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.txt6
-rw-r--r--misc/requirements/requirements-pyroma.txt16
-rw-r--r--misc/requirements/requirements-qutebrowser.txt-raw15
-rw-r--r--misc/requirements/requirements-sphinx.txt30
-rw-r--r--misc/requirements/requirements-tests-bleeding.txt7
-rw-r--r--misc/requirements/requirements-tests.txt93
-rw-r--r--misc/requirements/requirements-tests.txt-raw35
-rw-r--r--misc/requirements/requirements-tox.txt28
-rw-r--r--misc/requirements/requirements-tox.txt-raw10
-rw-r--r--misc/requirements/requirements-yamllint.txt6
-rw-r--r--misc/userscripts/README.md9
-rwxr-xr-xmisc/userscripts/cast31
-rwxr-xr-xmisc/userscripts/open_download23
-rwxr-xr-xmisc/userscripts/password_fill6
-rwxr-xr-xmisc/userscripts/qute-bitwarden41
-rwxr-xr-xmisc/userscripts/qute-keepassxc2
-rwxr-xr-xmisc/userscripts/qute-lastpass2
-rw-r--r--[-rwxr-xr-x]misc/userscripts/qute-pass36
-rwxr-xr-xmisc/userscripts/ripbang18
-rw-r--r--qutebrowser/__init__.py2
-rw-r--r--qutebrowser/browser/browsertab.py1
-rw-r--r--qutebrowser/browser/commands.py39
-rw-r--r--qutebrowser/browser/downloads.py36
-rw-r--r--qutebrowser/browser/greasemonkey.py1
-rw-r--r--qutebrowser/browser/hints.py38
-rw-r--r--qutebrowser/browser/history.py1
-rw-r--r--qutebrowser/browser/navigate.py3
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py19
-rw-r--r--qutebrowser/browser/shared.py12
-rw-r--r--qutebrowser/browser/signalfilter.py1
-rw-r--r--qutebrowser/browser/webelem.py5
-rw-r--r--qutebrowser/browser/webengine/notification.py24
-rw-r--r--qutebrowser/browser/webengine/webenginedownloads.py9
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py8
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py51
-rw-r--r--qutebrowser/browser/webkit/http.py11
-rw-r--r--qutebrowser/browser/webkit/webkithistory.py2
-rw-r--r--qutebrowser/browser/webkit/webkittab.py21
-rw-r--r--qutebrowser/browser/webkit/webview.py9
-rw-r--r--qutebrowser/commands/argparser.py2
-rw-r--r--qutebrowser/commands/command.py12
-rw-r--r--qutebrowser/commands/userscripts.py5
-rw-r--r--qutebrowser/completion/completer.py2
-rw-r--r--qutebrowser/completion/models/completionmodel.py1
-rw-r--r--qutebrowser/completion/models/configmodel.py3
-rw-r--r--qutebrowser/completion/models/miscmodels.py20
-rw-r--r--qutebrowser/components/braveadblock.py15
-rw-r--r--qutebrowser/components/hostblock.py21
-rw-r--r--qutebrowser/components/misccommands.py8
-rw-r--r--qutebrowser/components/utils/blockutils.py2
-rw-r--r--qutebrowser/config/config.py2
-rw-r--r--qutebrowser/config/configcommands.py13
-rw-r--r--qutebrowser/config/configdata.yml132
-rw-r--r--qutebrowser/config/configfiles.py4
-rw-r--r--qutebrowser/config/qtargs.py14
-rw-r--r--qutebrowser/config/websettings.py1
-rw-r--r--qutebrowser/extensions/loader.py42
-rw-r--r--qutebrowser/html/bindings.html2
-rw-r--r--qutebrowser/html/settings.html197
-rw-r--r--qutebrowser/html/warning-webkit.html6
-rw-r--r--qutebrowser/javascript/greasemonkey_wrapper.js20
-rw-r--r--qutebrowser/javascript/quirks/globalthis.user.js2
-rw-r--r--qutebrowser/keyinput/basekeyparser.py3
-rw-r--r--qutebrowser/keyinput/eventfilter.py2
-rw-r--r--qutebrowser/keyinput/keyutils.py2
-rw-r--r--qutebrowser/mainwindow/mainwindow.py6
-rw-r--r--qutebrowser/mainwindow/prompt.py17
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py36
-rw-r--r--qutebrowser/mainwindow/tabwidget.py43
-rw-r--r--qutebrowser/misc/crashsignal.py5
-rw-r--r--qutebrowser/misc/debugcachestats.py26
-rw-r--r--qutebrowser/misc/earlyinit.py18
-rw-r--r--qutebrowser/misc/editor.py2
-rw-r--r--qutebrowser/misc/guiprocess.py42
-rw-r--r--qutebrowser/misc/keyhintwidget.py1
-rw-r--r--qutebrowser/misc/lineparser.py1
-rw-r--r--qutebrowser/misc/miscwidgets.py2
-rw-r--r--qutebrowser/misc/msgbox.py1
-rw-r--r--qutebrowser/misc/quitter.py2
-rw-r--r--qutebrowser/misc/savemanager.py1
-rw-r--r--qutebrowser/misc/sessions.py9
-rw-r--r--qutebrowser/misc/split.py1
-rw-r--r--qutebrowser/misc/sql.py3
-rw-r--r--qutebrowser/misc/utilcmds.py5
-rw-r--r--qutebrowser/qutebrowser.py25
-rw-r--r--qutebrowser/utils/debug.py4
-rw-r--r--qutebrowser/utils/log.py14
-rw-r--r--qutebrowser/utils/message.py9
-rw-r--r--qutebrowser/utils/resources.py21
-rw-r--r--qutebrowser/utils/standarddir.py3
-rw-r--r--qutebrowser/utils/urlutils.py2
-rw-r--r--qutebrowser/utils/usertypes.py1
-rw-r--r--qutebrowser/utils/utils.py16
-rw-r--r--qutebrowser/utils/version.py8
-rw-r--r--requirements.txt23
-rwxr-xr-xscripts/dev/build_release.py69
-rw-r--r--scripts/dev/changelog_urls.json161
-rw-r--r--scripts/dev/gen_resources.py8
-rw-r--r--scripts/dev/misc_checks.py7
-rw-r--r--scripts/dev/pylint_checkers/qute_pylint/modeline.py6
-rw-r--r--scripts/dev/pylint_checkers/qute_pylint/openencoding.py83
-rw-r--r--scripts/dev/recompile_requirements.py259
-rw-r--r--scripts/dev/run_pylint_on_tests.py6
-rwxr-xr-xscripts/dev/src2asciidoc.py6
-rwxr-xr-xscripts/dev/update_3rdparty.py10
-rwxr-xr-xscripts/dictcli.py14
-rw-r--r--scripts/hostblock_blame.py4
-rw-r--r--scripts/opengl_info.py49
-rw-r--r--tests/conftest.py91
-rw-r--r--tests/end2end/conftest.py41
-rw-r--r--tests/end2end/data/brave-adblock/generate.py6
-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/data/hints/html/tabindex-negative.html13
-rw-r--r--tests/end2end/features/hints.feature5
-rw-r--r--tests/end2end/features/notifications.feature8
-rw-r--r--tests/end2end/features/search.feature20
-rw-r--r--tests/end2end/features/tabs.feature99
-rw-r--r--tests/end2end/features/utilcmds.feature1
-rw-r--r--tests/end2end/fixtures/quteprocess.py18
-rw-r--r--tests/end2end/fixtures/test_webserver.py5
-rw-r--r--tests/end2end/fixtures/webserver.py10
-rw-r--r--tests/end2end/fixtures/webserver_sub.py2
-rw-r--r--tests/end2end/test_hints_html.py15
-rw-r--r--tests/end2end/test_insert_mode.py2
-rw-r--r--tests/end2end/test_invocations.py82
-rw-r--r--tests/helpers/fixtures.py4
-rw-r--r--tests/helpers/logfail.py3
-rw-r--r--tests/helpers/testutils.py42
-rw-r--r--tests/unit/api/test_cmdutils.py3
-rw-r--r--tests/unit/browser/test_caret.py2
-rw-r--r--tests/unit/browser/test_history.py2
-rw-r--r--tests/unit/browser/test_inspector.py3
-rw-r--r--tests/unit/browser/webkit/test_webkitelem.py21
-rw-r--r--tests/unit/completion/test_completionwidget.py15
-rw-r--r--tests/unit/completion/test_models.py32
-rw-r--r--tests/unit/components/test_hostblock.py10
-rw-r--r--tests/unit/config/test_configcommands.py14
-rw-r--r--tests/unit/config/test_configfiles.py5
-rw-r--r--tests/unit/config/test_configtypes.py1
-rw-r--r--tests/unit/config/test_configutils.py12
-rw-r--r--tests/unit/config/test_qtargs.py28
-rw-r--r--tests/unit/config/test_websettings.py2
-rw-r--r--tests/unit/extensions/test_loader.py8
-rw-r--r--tests/unit/keyinput/test_basekeyparser.py11
-rw-r--r--tests/unit/mainwindow/statusbar/test_textbase.py6
-rw-r--r--tests/unit/misc/test_editor.py15
-rw-r--r--tests/unit/misc/test_guiprocess.py22
-rw-r--r--tests/unit/misc/test_keyhints.py4
-rw-r--r--tests/unit/misc/test_miscwidgets.py2
-rw-r--r--tests/unit/test_qutebrowser.py60
-rw-r--r--tests/unit/utils/test_log.py13
-rw-r--r--tests/unit/utils/test_qtutils.py10
-rw-r--r--tests/unit/utils/test_resources.py2
-rw-r--r--tests/unit/utils/test_standarddir.py12
-rw-r--r--tests/unit/utils/test_urlmatch.py157
-rw-r--r--tests/unit/utils/test_urlutils.py1
-rw-r--r--tests/unit/utils/test_utils.py5
-rw-r--r--tests/unit/utils/test_version.py23
-rw-r--r--tox.ini8
202 files changed, 2794 insertions, 1310 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index a95ba918d..cf1c019f7 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 2.3.0
+current_version = 2.4.0
commit = True
message = Release v{new_version}
tag = True
diff --git a/.flake8 b/.flake8
index 5cfa2e76c..9110dc54c 100644
--- a/.flake8
+++ b/.flake8
@@ -41,6 +41,9 @@ exclude = .*,__pycache__,resources.py
# W503: like break before binary operator
# W504: line break after binary operator
# FI15: __future__ import "generator_stop" missing
+# PT004: fixture '{name}' does not return anything, add leading underscore
+# PT011: pytest.raises(ValueError) is too broad, set the match parameter or use a more specific exception
+# PT012: pytest.raises() block should contain a single simple statement
ignore =
B001,B008,B305,
E128,E226,E265,E501,E402,E266,E722,E731,
@@ -49,8 +52,11 @@ ignore =
P101,P102,P103,
D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D412,D413,
A003,
- W503, W504
- FI15
+ W503, W504,
+ FI15,
+ PT004,
+ PT011,
+ PT012
min-version = 3.6.1
max-complexity = 12
per-file-ignores =
@@ -62,3 +68,6 @@ per-file-ignores =
copyright-check = True
copyright-regexp = # Copyright [\d-]+ .*
copyright-min-file-size = 110
+pytest-fixture-no-parentheses = True
+pytest-mark-no-parentheses = True
+pytest-parametrize-names-type = csv
diff --git a/.github/CONTRIBUTING.asciidoc b/.github/CONTRIBUTING.asciidoc
index bf9f0e66b..0d03af336 100644
--- a/.github/CONTRIBUTING.asciidoc
+++ b/.github/CONTRIBUTING.asciidoc
@@ -5,11 +5,11 @@ open pull requests.
- Before you start to work on something, please leave a comment on the relevant
issue (or open one). This makes sure there is no duplicate work done.
-- Either run the testsuite locally, or keep an eye on Travis CI / AppVeyor
- after pushing changes.
+- Either run the testsuite locally, or keep an eye on the CI at the end of the
+ pull request page after pushing changes.
- If you are stuck somewhere or have questions,
- https://github.com/qutebrowser/qutebrowser#getting-help[please ask]!
+ https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/index.asciidoc#getting-help[please ask]!
See the link:../doc/contributing.asciidoc[full contribution documentation] for
details and other useful hints.
diff --git a/.github/workflows/bleeding.yml b/.github/workflows/bleeding.yml
index 766f535d7..b2370357f 100644
--- a/.github/workflows/bleeding.yml
+++ b/.github/workflows/bleeding.yml
@@ -15,7 +15,7 @@ jobs:
container:
image: "qutebrowser/ci:archlinux-webengine-unstable"
env:
- PY_COLORS: "1"
+ FORCE_COLOR: "1"
DOCKER: "archlinux-webengine-unstable"
CI: true
PYTEST_ADDOPTS: "--color=yes"
@@ -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/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e50ba2c60..afcf720e4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,7 +6,7 @@ on:
- 'dependabot/*'
pull_request:
env:
- PY_COLORS: "1"
+ FORCE_COLOR: "1"
MYPY_FORCE_TERMINAL_WIDTH: "180"
jobs:
@@ -43,7 +43,7 @@ jobs:
key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('scripts/dev/pylint_checkers/qute_pylint/*.py') }}"
- uses: actions/setup-python@v2
with:
- python-version: '3.8'
+ python-version: '3.10'
- uses: actions/setup-node@v2-beta
with:
node-version: '12.x'
@@ -53,7 +53,7 @@ jobs:
- name: Install dependencies
run: |
[[ ${{ matrix.testenv }} == eslint ]] && npm install -g eslint
- [[ ${{ matrix.testenv }} == docs ]] && sudo apt-get install --no-install-recommends asciidoc
+ [[ ${{ matrix.testenv }} == docs ]] && sudo apt-get update && sudo apt-get install --no-install-recommends asciidoc
if [[ ${{ matrix.testenv }} == shellcheck ]]; then
scversion="stable"
bindir="$HOME/.local/bin"
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/.mypy.ini b/.mypy.ini
index 289f3eb87..501ab747e 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -20,6 +20,7 @@ strict_equality = True
### Other strictness flags
warn_unreachable = True
disallow_any_unimported = True
+enable_error_code = ignore-without-code
### Output
show_error_codes = True
diff --git a/.pylintrc b/.pylintrc
index b771078f3..4ff4f2080 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -5,15 +5,20 @@ ignore=resources.py
extension-pkg-whitelist=PyQt5,sip
load-plugins=qute_pylint.config,
qute_pylint.modeline,
- qute_pylint.openencoding,
pylint.extensions.docstyle,
pylint.extensions.emptystring,
- pylint.extensions.broad_try_clause,
pylint.extensions.overlapping_exceptions,
-persistent=n
+ pylint.extensions.code_style,
+ pylint.extensions.comparison_placement,
+ pylint.extensions.for_any_all,
+ pylint.extensions.docstyle,
+ pylint.extensions.check_elif,
+ pylint.extensions.typing,
+ pylint.extensions.docparams,
+ pylint.extensions.private_import,
-[broad_try_clause]
-max-try-statements=7
+persistent=n
+py-version=3.6
[MESSAGES CONTROL]
enable=all
@@ -46,7 +51,16 @@ disable=locally-disabled,
too-many-statements,
too-few-public-methods,
import-outside-toplevel,
- bad-continuation # This lint disagrees with Black
+ bad-continuation, # This lint disagrees with Black
+ consider-using-f-string,
+ logging-fstring-interpolation,
+ raise-missing-from,
+ consider-using-tuple,
+ consider-using-namedtuple-or-dataclass,
+ missing-raises-doc,
+ missing-type-doc,
+ missing-param-doc,
+ useless-param-doc,
[BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$
@@ -58,6 +72,7 @@ argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$
docstring-min-length=3
no-docstring-rgx=(^_|^main$)
+class-const-naming-style = snake_case
[FORMAT]
max-line-length=88
diff --git a/README.asciidoc b/README.asciidoc
index 80d8b3880..bb1f2562c 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -63,7 +63,7 @@ ways:
* Use the built-in `:report` command or the automatic crash dialog.
* Open an issue in the Github issue tracker.
* Write a mail to the
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].
For security bugs, please contact me directly at mail@qutebrowser.org, GPG ID
@@ -75,8 +75,8 @@ Requirements
The following software and libraries are required to run qutebrowser:
* https://www.python.org/[Python] 3.6.1 or newer
-* https://www.qt.io/[Qt] 5.12.0 or newer (5.12 LTS or 5.15 recommended)
- with the following modules:
+* https://www.qt.io/[Qt] 5.12.0 or newer (5.12 LTS or 5.15 recommended, Qt 6 is
+ not supported yet) with the following modules:
- QtCore / qtbase
- QtQuick (part of qtbase or qtdeclarative in some distributions)
- QtSQL (part of qtbase in some distributions)
@@ -203,20 +203,16 @@ Active
* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
-* https://nyxt.atlas.engineer/[Nyxt browser] (formerly "Next browser", Lisp, Emacs-like but also offers Vim bindings, QtWebKit or GTK+/WebKit2 - note there was a https://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly)
+* 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)
* Chrome/Chromium addons:
- https://vimium.github.io/[Vimium],
- https://github.com/dcchambers/vb4c[vb4c] (fork of cVim)
+ https://vimium.github.io/[Vimium]
* Firefox addons (based on WebExtensions):
- https://github.com/tridactyl/tridactyl[Tridactyl],
- https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental),
- https://github.com/ueokande/vim-vixen[Vim Vixen],
- https://github.com/amedama41/vvimpulation[VVimpulation]
+ https://tridactyl.xyz/[Tridactyl],
+ https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF]
* Addons for Firefox and Chrome:
https://github.com/brookhong/Surfingkeys[Surfingkeys],
- https://krabby.netlify.com/[Krabby],
https://lydell.github.io/LinkHints/[Link Hints] (hinting only)
* Addons for Safari:
https://televator.net/vimari/[Vimari]
@@ -231,23 +227,27 @@ main inspiration for qutebrowser)
QtWebEngine, https://github.com/parkouss/webmacs/issues/137[unmaintained])
* https://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
WebKit1)
-* https://wiki.archlinux.org/index.php?title=Jumanji[jumanji] (C, GTK+ with WebKit1,
-original site is gone but the Arch Linux wiki has some data)
+* https://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1)
* http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko)
* 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],
https://github.com/akhodakivskiy/VimFx[VimFx] (seems to offer a
https://gir.st/blog/legacyfox.htm[hack] to run on modern Firefox releases),
- https://github.com/shinglyu/QuantumVim[QuantumVim]
+ https://github.com/shinglyu/QuantumVim[QuantumVim],
+ https://github.com/ueokande/vim-vixen[Vim Vixen] (ESR only),
+ https://github.com/amedama41/vvimpulation[VVimpulation],
+ https://krabby.netlify.com/[Krabby]
* Chrome/Chromium addons:
https://github.com/k2nr/ViChrome/[ViChrome],
https://github.com/jinzhu/vrome[Vrome],
https://github.com/lusakasa/saka-key[Saka Key] (https://github.com/lusakasa/saka-key/issues/171[unmaintained]),
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]
License
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 008c693a3..1c2c92e0b 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -15,17 +15,146 @@ 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.
+- The default `hint.selectors` now match more ARIA roles (`tab`, `checkbox`,
+ `menuitem`, `menuitemcheckbox` and `menuitemradio`).
+- Using e.g. `:bind --mode=passthrough` now scrolls to the passthrough section
+ on the `qute://bindings` page.
+- Clicking on a notification now tries to focus the tab where the notification
+ is coming from. Note this might not work properly if there is more than one
+ tab from the same host open.
+- Improvements to userscripts:
+ * `qute-bitwarden` understands a new `--password-prompt-invocation`, which can
+ be used to specify a tool other than `rofi` to ask for a password.
+ * `cast` now uses `yt-dlp` if available (falling back to `youtube-dl` if not).
+ It also lets users override the tool to use via a `QUTE_CAST_YTDL_PROGRAM`
+ environment variable.
+ * `qute-pass` now understands a new `--prefix` argument if used in gopass
+ mode, which gets passed as subfolder prefix to `gopass`.
+ * `ripbang` now works again (it got blocked due to a missing user agent and
+ used outdated qutebrowser commands before)
+ * `open_download` now supports Flatpak by using its XDG Desktop Portal.
+ * `open_download` now waits for the exit status of `xdg-open`, causing
+ qutebrowser to report any issues with it.
+- The `content.headers.custom` setting now accepts empty strings as values,
+ resulting in an empty header being sent.
+- Renamed settings:
+ * `qt.low_end_device_mode` -> `qt.chromium.low_end_device_mode`
+ * `qt.process_model` -> `qt.chromium.process_model`
+- System-wide userscripts are now discovered from the correct location when
+ running via Flatpak (`/app/share` rather than `/usr/share`).
+
+Added
+~~~~~
+
+- New `input.match_counts` option which allows to turn off count matching for
+ more emacs-like bindings.
+- New `{relative_index}` field for `tabs.title.format` (and `.pinned_format`)
+ which shows relative tab numbers.
+- New `input.mode_override` option which allows overriding the current mode
+ based on the new URL when navigating or switching tabs.
+- New `qt.chromium.sandboxing` setting which allows to disable Chromium's
+ sandboxing (mainly intended for development and testing)
+- New `QUTE_TAB_INDEX` variable for userscripts, containing the index of the
+ current tab.
+- New `editor.remove_file` setting which can be set to `False` to keep all
+ temporary editor files after closing the external editor.
+
+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.
+- Elements getting a hint due to a `tabindex` now are skipped if it's set to
+ `-1`, reducing some false-positives.
+- The audible indicator (`[A]`) now uses a 2s cooldown when the audio goes
+ silent, equivalent with the behavior of older QtWebEngine versions.
+- With `confirm_quit` set to `downloads`, the confirmation dialog is now only
+ shown when closing the last window (rather than closing any window, which
+ would continue running that window's downloads). Unfortunately, more issues
+ with `confirm_quit` and multiple windows remain.
+- Crash when a previous crash-log file contains non-ASCII characters (which
+ should never happen unless it was edited manually)
+- Due to changes in Debian, an old workaround (for broken QtWebEngine patching
+ on Debian) caused the inferior qutebrowser error page to be displayed, when
+ Chromium's would have worked fine. The workaround was now dropped.
+- Crash when using `<Ctrl-D>` (`:completion-item-del`) in the `:tab-focus`
+ list, rather than `:tab-select`.
+- Work around a Qt issue causing `:spawn` to run executables from the current
+ directory if no system-wide executable was found. The underlying Qt bug is
+ tracked as [CVE-2022-25255](https://lists.qt-project.org/pipermail/announce/2022-February/000333.html),
+ though the impact with typical qutebrowser usage is low: Normally,
+ qutebrowser is run from a fixed location (usually the users home directory),
+ and `:spawn` is not typically used with executables that don't exist. The main
+ security impact of this bug is in tools like text editors, which are often
+ executed in untrusted directories and might attempt to run auxiliary tools
+ automatically.
+
+[[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.
+- Added a site-specific quirk to make cookie dialogs on StackExchange pages
+ (such as Stack Overflow) work on Qt 5.12.
+
[[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
+~~~~~
+
+- New `content.blocking.hosts.block_subdomains` setting which can be used to
+ disable the subdomain blocking for the hosts-based adblocker introduced in
+ v2.3.0.
+- New `downloads.prevent_mixed_content` setting to prevent insecure
+ mixed-content downloads (true by default).
+- New `--private` flag for `:tab-clone`, which clones a tab into a new private
+ window, mirroring the same flags for `:open` and `:tab-give`.
+
Fixed
~~~~~
- Switching tabs via mouse wheel scrolling now works properly on macOS. Set
`tabs.mousewheel_switching` to false if you prefer the previous behavior.
-- Crash when entering unicode surrogates into the filename prompt.
-- `UnboundLocalError` in `qute-keepass` when the database couldn't be opened.
+- Speculative fix for a crash when closing qutebrowser while a systray
+ notification is shown.
Changed
~~~~~~~
@@ -34,15 +163,33 @@ Changed
- When opening a file qutebrowser can't handle from a `file:///` directory
listing, qutebrowser now opens it with the default application rather than
displaying a download prompt.
+- In Greasemonkey scripts, using "overrideMimeType" with GM_xmlhttpRequest is
+ now supported.
+- `:hint --rapid` is now supported for the `tab` hinting target no matter what
+ `tabs.background` is set to, as there are various scenarios where tabs can
+ open in the background.
+- New flags for the `qute-pass` userscript:
+ * `--unfiltered` to show all secrets, not just the one matching the current
+ URL.
+ * `--always-show-selection` to confirm the password to be entered even if
+ there's only a single match.
+- In insert mode, `<Shift-Escape>` is now bound to `fake-key <Escape>` by
+ default, i.e., sends an Escape keypress to the website.
+- Using `GM_setClipboard` in Greasemonkey scripts is now supported.
[[v2.3.1]]
-v2.3.1 (unreleased)
+v2.3.1 (2021-07-28)
-------------------
Fixed
~~~~~
+- Updated the workaround for Google Account log in claiming that this browser
+ isn't secure. For an equivalent workaround on older versions, run:
+ `:set -u https://accounts.google.com/* content.headers.user_agent "Mozilla/5.0 ({os_info}; rv:90.0) Gecko/20100101 Firefox/90.0"`
- Corrupt cache file exceptions with `adblock` 0.5.0+ are now handled properly.
+- Crash when entering unicode surrogates into the filename prompt.
+- `UnboundLocalError` in `qute-keepass` when the database couldn't be opened.
[[v2.3.0]]
v2.3.0 (2021-06-28)
@@ -4138,7 +4285,7 @@ v0.1.4 (2015-03-19)
Changed
~~~~~~~
-* The Windows builds come with Qt 5.4.1 which has some https://lists.schokokeks.org/pipermail/qutebrowser/2015-March/000054.html[related bugfixes].
+* The Windows builds come with Qt 5.4.1 which has some https://listi.jpberlin.de/pipermail/qutebrowser/2015-March/000054.html[related bugfixes].
* Improvements to CPU usage when idle.
* Ensure there's no size for `font-family` settings.
* Handle URLs with double-colon as search strings.
diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc
index 1f87e9163..75c19045e 100644
--- a/doc/contributing.asciidoc
+++ b/doc/contributing.asciidoc
@@ -24,7 +24,7 @@ several ways:
* Send a mail to the mailing list at mailto:qutebrowser@lists.qutebrowser.org[]
(optionally
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[subscribe]
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser[subscribe]
first).
* Join the IRC channel link:ircs://irc.libera.chat:6697/#qutebrowser[`#qutebrowser`] on
https://libera.chat/[Libera Chat] (https://web.libera.chat/#qutebrowser[webchat],
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index f570a3ffd..442c136a7 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -294,7 +294,7 @@ Cycle an option between multiple values.
* +'values'+: The values to cycle through.
==== optional arguments
-* +*-u*+, +*--pattern*+: The URL pattern to use.
+* +*-u*+, +*--pattern*+: The link:configuring{outfilesuffix}#patterns[URL pattern] to use.
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting.
@@ -391,7 +391,7 @@ This sets an option back to its default and removes it from autoconfig.yml.
* +'option'+: The name of the option.
==== optional arguments
-* +*-u*+, +*--pattern*+: The URL pattern to use.
+* +*-u*+, +*--pattern*+: The link:configuring{outfilesuffix}#patterns[URL pattern] to use.
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
[[config-write-py]]
@@ -679,9 +679,8 @@ Start hinting.
* +*-a*+, +*--add-history*+: Whether to add the spawned or yanked link to the browsing history.
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. With rapid hinting, the hint mode isn't left after a hint is followed, so you can easily
- open multiple links. This is only possible with targets
- `tab` (with `tabs.background=true`), `tab-bg`,
- `window`, `run`, `hover`, `userscript` and `spawn`.
+ open multiple links. Note this won't work with targets
+ `tab-fg`, `fill`, `delete` and `right-click`.
* +*-f*+, +*--first*+: Click the first hinted element without prompting.
@@ -1286,7 +1285,7 @@ If the option name ends with '?' or no value is provided, the value of the optio
==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting.
-* +*-u*+, +*--pattern*+: The URL pattern to use.
+* +*-u*+, +*--pattern*+: The link:configuring{outfilesuffix}#patterns[URL pattern] to use.
[[set-cmd-text]]
=== set-cmd-text
@@ -1357,13 +1356,14 @@ The tab index to stop.
[[tab-clone]]
=== tab-clone
-Syntax: +:tab-clone [*--bg*] [*--window*]+
+Syntax: +:tab-clone [*--bg*] [*--window*] [*--private*]+
Duplicate the current tab.
==== optional arguments
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
+* +*-p*+, +*--private*+: Open in a new private window.
[[tab-close]]
=== tab-close
@@ -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 e1a57cdfb..b469118ed 100644
--- a/doc/help/configuring.asciidoc
+++ b/doc/help/configuring.asciidoc
@@ -22,6 +22,19 @@ exists, the `autoconfig.yml` file **is not read anymore** by default. You need
to <<configpy-autoconfig,load it from `config.py`>> if you want settings changed via
`:set`/`:bind` to persist between restarts.
+[[patterns]]
+URL pattern support
+-------------------
+
+Many settings are customizable depending on the page being visited by using URL
+patterns. The link:settings{outfilesuffix}[settings documentation] marks such
+settings with "This setting supports URL patterns.
+
+The syntax is based on Chromium's
+https://developer.chrome.com/docs/extensions/mv3/match_patterns/[URL pattern syntax].
+As an extension, the scheme and path can be left off as a short-hand syntax, so
+`example.com` is equivalent to `*://example.com/*`.
+
[[autoconfig]]
Configuring qutebrowser via the user interface
----------------------------------------------
@@ -45,9 +58,10 @@ customizable.
Using the link:commands{outfilesuffix}#set[`:set`] command and command completion, you
can quickly set settings interactively, for example `:set tabs.position left`.
-Some settings are also customizable for a given
-https://developer.chrome.com/apps/match_patterns[URL pattern] by doing e.g.
-`:set --pattern=*://example.com/ content.images false`.
+<<patterns,URL patterns>> can be used via
+`:set --pattern *://example.com/* content.images false`, or with shorthand
+syntax for both argument and pattern, `:set -u example.com content.images
+false`.
To get more help about a setting, use e.g. `:help tabs.position`.
@@ -160,8 +174,8 @@ color = config.get('colors.completion.fg')
Per-domain settings
~~~~~~~~~~~~~~~~~~~
-Using `config.set`, some settings are also customizable for a given
-https://developer.chrome.com/apps/match_patterns[URL pattern]:
+Using `config.set` instead of the `c.` shorthand, many settings are also
+customizable for a given <<patterns,URL patterns>>.
[source,python]
----
@@ -392,11 +406,13 @@ Pre-built colorschemes
^^^^^^^^^^^^^^^^^^^^^^
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
-- https://gitlab.com/jjzmajic/qutewal[Pywal integration]
+- Another collection: https://github.com/leosolid/qutebrowser-themes[qutebrowser-themes]
+- Pywal integration: https://gitlab.com/jjzmajic/qutewal[qutewal], https://github.com/makman12/pywalQute[pywalQute]
- https://github.com/arcticicestudio/nord[Nord]: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
- 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://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[gruvbox]
+- https://www.opencode.net/wakellor957/qb-breath/-/blob/main/qb-breath.py[Manjaro Breath-like]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
diff --git a/doc/help/index.asciidoc b/doc/help/index.asciidoc
index c7fb88c8d..127cc5d86 100644
--- a/doc/help/index.asciidoc
+++ b/doc/help/index.asciidoc
@@ -26,10 +26,10 @@ link:ircs://irc.libera.chat:6697/#qutebrowser[`#qutebrowser`] on
https://libera.chat/[Libera Chat]
(https://web.libera.chat/#qutebrowser[webchat], https://matrix.to/#qutebrowser:libera.chat[via Matrix]),
or by writing a message to the
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].
-There's also an https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[announce-only mailinglist]
+There's also an https://listi.jpberlin.de/mailman/listinfo/qutebrowser-announce[announce-only mailinglist]
at mailto:qutebrowser-announce@lists.qutebrowser.org[] (the announcements also
get sent to the general qutebrowser@ list).
@@ -49,7 +49,7 @@ ways:
* Use the built-in `:report` command or the automatic crash dialog.
* Open an issue in the Github issue tracker.
* Write a mail to the
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].
Other resources
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index c5206e52e..bdc5bef99 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -142,6 +142,7 @@
|<<content.autoplay,content.autoplay>>|Automatically start playing `<video>` elements.
|<<content.blocking.adblock.lists,content.blocking.adblock.lists>>|List of URLs to ABP-style adblocking rulesets.
|<<content.blocking.enabled,content.blocking.enabled>>|Enable the ad/host blocker
+|<<content.blocking.hosts.block_subdomains,content.blocking.hosts.block_subdomains>>|Block subdomains of blocked hosts.
|<<content.blocking.hosts.lists,content.blocking.hosts.lists>>|List of URLs to host blocklists for the host blocker.
|<<content.blocking.method,content.blocking.method>>|Which method of blocking ads should be used.
|<<content.blocking.whitelist,content.blocking.whitelist>>|A list of patterns that should always be loaded, despite being blocked by the ad-/host-blocker.
@@ -208,9 +209,11 @@
|<<downloads.location.suggestion,downloads.location.suggestion>>|What to display in the download filename input.
|<<downloads.open_dispatcher,downloads.open_dispatcher>>|Default program used to open downloads.
|<<downloads.position,downloads.position>>|Where to show the downloaded files.
+|<<downloads.prevent_mixed_content,downloads.prevent_mixed_content>>|Automatically abort insecure (HTTP) downloads originating from secure (HTTPS) pages.
|<<downloads.remove_finished,downloads.remove_finished>>|Duration (in milliseconds) to wait before removing finished downloads.
|<<editor.command,editor.command>>|Editor (and arguments) to use for the `edit-*` commands.
|<<editor.encoding,editor.encoding>>|Encoding to use for the editor.
+|<<editor.remove_file,editor.remove_file>>|Delete the temporary file upon closing the editor.
|<<fileselect.folder.command,fileselect.folder.command>>|Command (and arguments) to use for selecting a single folder in forms. The command should write the selected folder path to the specified file or stdout.
|<<fileselect.handler,fileselect.handler>>|Handler for selecting file(s) in forms. If `external`, then the commands specified by `fileselect.single_file.command` and `fileselect.multiple_files.command` are used to select one or multiple files respectively.
|<<fileselect.multiple_files.command,fileselect.multiple_files.command>>|Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file or to stdout, separated by newlines.
@@ -267,7 +270,9 @@
|<<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.mode_override,input.mode_override>>|Mode to change to when focusing on a tab/URL changes.
|<<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.
|<<input.partial_timeout,input.partial_timeout>>|Timeout (in milliseconds) for partially typed key bindings.
@@ -283,13 +288,14 @@
|<<prompt.filebrowser,prompt.filebrowser>>|Show a filebrowser in download prompts.
|<<prompt.radius,prompt.radius>>|Rounding radius (in pixels) for the edges of prompts.
|<<qt.args,qt.args>>|Additional arguments to pass to Qt, without leading `--`.
+|<<qt.chromium.low_end_device_mode,qt.chromium.low_end_device_mode>>|When to use Chromium's low-end device mode.
+|<<qt.chromium.process_model,qt.chromium.process_model>>|Which Chromium process model to use.
+|<<qt.chromium.sandboxing,qt.chromium.sandboxing>>|What sandboxing mechanisms in Chromium to use.
|<<qt.environ,qt.environ>>|Additional environment variables to set.
|<<qt.force_platform,qt.force_platform>>|Force a Qt platform to use.
|<<qt.force_platformtheme,qt.force_platformtheme>>|Force a Qt platformtheme to use.
|<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine.
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
-|<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode.
-|<<qt.process_model,qt.process_model>>|Which Chromium process model to use.
|<<qt.workarounds.locale,qt.workarounds.locale>>|Work around locale parsing issues in QtWebEngine 5.15.3.
|<<qt.workarounds.remove_service_workers,qt.workarounds.remove_service_workers>>|Delete the QtWebEngine Service Worker directory on every start.
|<<scrolling.bar,scrolling.bar>>|When/how to show the scrollbar.
@@ -435,9 +441,7 @@ The following modes are available:
that single keys can't be bound in this mode.
* passthrough: Similar to insert mode, but passes through all keypresses
- except `<Escape>` to leave the mode. It might be useful to bind
- `<Escape>` to some other key in this mode if you want to be able to send
- an Escape key to the website as well. Note that single keys can't be
+ except `<Shift+Escape>` to leave the mode. Note that single keys can't be
bound in this mode.
* command: Entered when pressing the `:` key in order to enter a command.
@@ -544,6 +548,7 @@ Default:
* +pass:[&lt;Ctrl-E&gt;]+: +pass:[edit-text]+
* +pass:[&lt;Escape&gt;]+: +pass:[mode-leave]+
+* +pass:[&lt;Shift-Escape&gt;]+: +pass:[fake-key &lt;Escape&gt;]+
* +pass:[&lt;Shift-Ins&gt;]+: +pass:[insert-text -- {primary}]+
- +pass:[normal]+:
@@ -1956,7 +1961,7 @@ Default:
=== content.autoplay
Automatically start playing `<video>` elements.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -1988,7 +1993,16 @@ Default:
=== content.blocking.enabled
Enable the ad/host blocker
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
+
+Type: <<types,Bool>>
+
+Default: +pass:[true]+
+
+[[content.blocking.hosts.block_subdomains]]
+=== content.blocking.hosts.block_subdomains
+Block subdomains of blocked hosts.
+Note: If only a single subdomain is blocked but should be allowed, consider using `content.blocking.whitelist` instead.
Type: <<types,Bool>>
@@ -2054,7 +2068,7 @@ Default: empty
Enable support for the HTML 5 web application cache feature.
An application cache acts like an HTTP cache in some sense. For documents that use the application cache via JavaScript, the loader engine will first ask the application cache for the contents, before hitting the network.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebKit backend.
@@ -2105,7 +2119,7 @@ If this setting is used with URL patterns, the pattern gets applied to the origi
With QtWebEngine 5.15.0+, paths will be stripped from URLs, so URL patterns using paths will not match. With QtWebEngine 5.15.2+, subdomains are additionally stripped as well, so you will typically need to set this setting for `example.com` when the cookie is set on `somesubdomain.example.com` for it to work properly.
To debug issues with this setting, start qutebrowser with `--debug --logfilter network --debug-flag log-cookies` which will show all cookies being set.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,String>>
@@ -2139,7 +2153,7 @@ Default: +pass:[iso-8859-1]+
=== content.desktop_capture
Allow websites to share screen content.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,BoolAsk>>
@@ -2155,7 +2169,7 @@ Default: +pass:[ask]+
=== content.dns_prefetch
Try to pre-fetch DNS entries to speed up browsing.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2168,7 +2182,7 @@ Default: +pass:[true]+
Expand each subframe to its contents.
This will flatten all the frames to become one scrollable page.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebKit backend.
@@ -2197,7 +2211,7 @@ Default: +pass:[false]+
=== content.geolocation
Allow websites to request geolocations.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,BoolAsk>>
@@ -2214,7 +2228,7 @@ Default: +pass:[ask]+
Value to send in the `Accept-Language` header.
Note that the value read from JavaScript is always the global value.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,String>>
@@ -2224,7 +2238,7 @@ Default: +pass:[en-US,en;q=0.9]+
=== content.headers.custom
Custom headers for qutebrowser HTTP requests.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Dict>>
@@ -2235,7 +2249,7 @@ Default: empty
Value to send in the `DNT` header.
When this is set to true, qutebrowser asks websites to not track your identity. If set to null, the DNT header is not sent at all.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2244,7 +2258,7 @@ Default: +pass:[true]+
[[content.headers.referer]]
=== content.headers.referer
When to send the Referer header.
-The Referer header tells websites from which website you were coming from when visiting them.
+The Referer header tells websites from which website you were coming from when visiting them. Note that with QtWebEngine, websites can override this preference by setting the `Referrer-Policy:` header, so that any websites visited from them get the full referer.
No restart is needed with QtWebKit.
This setting requires a restart.
@@ -2283,7 +2297,7 @@ QtWebEngine between 5.12 and 5.14 (inclusive), changing the value exposed
to JavaScript requires a restart.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,FormatString>>
@@ -2293,7 +2307,7 @@ Default: +pass:[Mozilla/5.0 ({os_info}) AppleWebKit/{webkit_version} (KHTML, lik
=== content.hyperlink_auditing
Enable hyperlink auditing (`<a ping>`).
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2303,7 +2317,7 @@ Default: +pass:[false]+
=== content.images
Load images automatically in web pages.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2322,7 +2336,7 @@ Default: +pass:[true]+
Allow JavaScript to read from or write to the clipboard.
With QtWebEngine, writing the clipboard as response to a user interaction is always allowed.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2332,7 +2346,7 @@ Default: +pass:[false]+
=== content.javascript.can_close_tabs
Allow JavaScript to close tabs.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebKit backend.
@@ -2344,7 +2358,7 @@ Default: +pass:[false]+
=== content.javascript.can_open_tabs_automatically
Allow JavaScript to open new tabs without user interaction.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2354,7 +2368,7 @@ Default: +pass:[false]+
=== content.javascript.enabled
Enable JavaScript.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2396,7 +2410,7 @@ Default: +pass:[true]+
=== content.local_content_can_access_file_urls
Allow locally loaded documents to access other local URLs.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2406,7 +2420,7 @@ Default: +pass:[true]+
=== content.local_content_can_access_remote_urls
Allow locally loaded documents to access remote URLs.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2416,7 +2430,7 @@ Default: +pass:[false]+
=== content.local_storage
Enable support for HTML 5 local storage and Web SQL.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2426,7 +2440,7 @@ Default: +pass:[true]+
=== content.media.audio_capture
Allow websites to record audio.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2444,7 +2458,7 @@ Default: +pass:[ask]+
=== content.media.audio_video_capture
Allow websites to record audio and video.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2462,7 +2476,7 @@ Default: +pass:[ask]+
=== content.media.video_capture
Allow websites to record video.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2480,7 +2494,7 @@ Default: +pass:[ask]+
=== content.mouse_lock
Allow websites to lock your mouse pointer.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2499,7 +2513,7 @@ Default: +pass:[ask]+
Automatically mute tabs.
Note that if the `:tab-mute` command is used, the mute status for the affected tab is now controlled manually, and this setting doesn't have any effect.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2518,7 +2532,7 @@ Default: empty
=== content.notifications.enabled
Allow websites to show notifications.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
On QtWebEngine, this setting requires Qt 5.13 or newer.
@@ -2568,7 +2582,7 @@ Whether to show the origin URL for notifications.
Note that URL patterns with this setting only get matched against the origin part of the URL, so e.g. paths in patterns will never match.
Note that with the `qt` presenter, origins are never shown.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
On QtWebEngine, this setting requires Qt 5.14 or newer.
@@ -2583,7 +2597,7 @@ Default: +pass:[true]+
Allow pdf.js to view PDF files in the browser.
Note that the files can still be downloaded by clicking the download button in the pdf.js viewer.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2593,7 +2607,7 @@ Default: +pass:[false]+
=== content.persistent_storage
Allow websites to request persistent storage quota via `navigator.webkitPersistentStorage.requestQuota`.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2611,7 +2625,7 @@ Default: +pass:[ask]+
=== content.plugins
Enable plugins in Web pages.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2637,7 +2651,7 @@ Default: +pass:[false]+
=== content.print_element_backgrounds
Draw the background color and images also when the page is printed.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2657,7 +2671,7 @@ Default: +pass:[false]+
=== content.proxy
Proxy to use.
In addition to the listed values, you can use a `socks://...` or `http://...` URL.
-Note that with QtWebEngine, it will take a couple of seconds until the change is applied, if this value is changed at runtime.
+Note that with QtWebEngine, it will take a couple of seconds until the change is applied, if this value is changed at runtime. Authentication for SOCKS proxies isn't supported due to Chromium limitations.
Type: <<types,Proxy>>
@@ -2682,7 +2696,7 @@ Default: +pass:[true]+
=== content.register_protocol_handler
Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2735,7 +2749,7 @@ Default:
=== content.tls.certificate_errors
How to proceed on TLS certificate errors.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,String>>
@@ -2752,7 +2766,7 @@ Default: +pass:[ask]+
=== content.unknown_url_scheme_policy
How navigation requests to URLs with unknown schemes are handled.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebEngine backend.
@@ -2778,7 +2792,7 @@ Default: empty
=== content.webgl
Enable WebGL.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2809,7 +2823,7 @@ Monitor load requests for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the devtools JavaScript console.
Note that bypasses for the XSS auditor are widely known and it can be abused for cross-site info leaks in some scenarios, see: https://www.chromium.org/developers/design-documents/xss-auditor
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -2878,6 +2892,17 @@ Valid values:
Default: +pass:[top]+
+[[downloads.prevent_mixed_content]]
+=== downloads.prevent_mixed_content
+Automatically abort insecure (HTTP) downloads originating from secure (HTTPS) pages.
+For per-domain settings, the relevant URL is the URL initiating the download, not the URL the download itself is coming from. It's not recommended to set this setting to false globally.
+
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
+
+Type: <<types,Bool>>
+
+Default: +pass:[true]+
+
[[downloads.remove_finished]]
=== downloads.remove_finished
Duration (in milliseconds) to wait before removing finished downloads.
@@ -2917,6 +2942,14 @@ Type: <<types,Encoding>>
Default: +pass:[utf-8]+
+[[editor.remove_file]]
+=== editor.remove_file
+Delete the temporary file upon closing the editor.
+
+Type: <<types,Bool>>
+
+Default: +pass:[true]+
+
[[fileselect.folder.command]]
=== fileselect.folder.command
Command (and arguments) to use for selecting a single folder in forms. The command should write the selected folder path to the specified file or stdout.
@@ -3115,7 +3148,7 @@ Default: +pass:[default_size default_family]+
=== fonts.web.family.cursive
Font family for cursive fonts.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,FontFamily>>
@@ -3125,7 +3158,7 @@ Default: empty
=== fonts.web.family.fantasy
Font family for fantasy fonts.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,FontFamily>>
@@ -3135,7 +3168,7 @@ Default: empty
=== fonts.web.family.fixed
Font family for fixed fonts.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,FontFamily>>
@@ -3145,7 +3178,7 @@ Default: empty
=== fonts.web.family.sans_serif
Font family for sans-serif fonts.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,FontFamily>>
@@ -3155,7 +3188,7 @@ Default: empty
=== fonts.web.family.serif
Font family for serif fonts.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,FontFamily>>
@@ -3165,7 +3198,7 @@ Default: empty
=== fonts.web.family.standard
Font family for standard fonts.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,FontFamily>>
@@ -3175,7 +3208,7 @@ Default: empty
=== fonts.web.size.default
Default font size (in pixels) for regular text.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Int>>
@@ -3185,7 +3218,7 @@ Default: +pass:[16]+
=== fonts.web.size.default_fixed
Default font size (in pixels) for fixed-pitch text.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Int>>
@@ -3195,7 +3228,7 @@ Default: +pass:[13]+
=== fonts.web.size.minimum
Hard minimum font size (in pixels).
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Int>>
@@ -3205,7 +3238,7 @@ Default: +pass:[0]+
=== fonts.web.size.minimum_logical
Minimum logical font size (in pixels) that is applied when zooming out.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Int>>
@@ -3374,7 +3407,7 @@ Default: +pass:[true]+
=== hints.selectors
CSS selectors used to determine which elements on a page should have hints.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting can only be set in config.py.
@@ -3401,11 +3434,16 @@ Default:
* +pass:[[role=&quot;link&quot;\]]+
* +pass:[[role=&quot;option&quot;\]]+
* +pass:[[role=&quot;button&quot;\]]+
+* +pass:[[role=&quot;tab&quot;\]]+
+* +pass:[[role=&quot;checkbox&quot;\]]+
+* +pass:[[role=&quot;menuitem&quot;\]]+
+* +pass:[[role=&quot;menuitemcheckbox&quot;\]]+
+* +pass:[[role=&quot;menuitemradio&quot;\]]+
* +pass:[[ng-click\]]+
* +pass:[[ngClick\]]+
* +pass:[[data-ng-click\]]+
* +pass:[[x-ng-click\]]+
-* +pass:[[tabindex\]]+
+* +pass:[[tabindex\]:not([tabindex=&quot;-1&quot;\])]+
- +pass:[images]+:
* +pass:[img]+
@@ -3510,7 +3548,7 @@ Default: +pass:[false]+
Leave insert mode when starting a new page load.
Patterns may be unreliable on this setting, and they may match the url you are navigating to, or the URL you are navigating from.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -3528,7 +3566,16 @@ Default: +pass:[false]+
=== input.links_included_in_focus_chain
Include hyperlinks in the keyboard focus chain when tabbing.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
+
+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>>
@@ -3549,6 +3596,22 @@ Type: <<types,Bool>>
Default: +pass:[true]+
+[[input.mode_override]]
+=== input.mode_override
+Mode to change to when focusing on a tab/URL changes.
+
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
+
+Type: <<types,String>>
+
+Valid values:
+
+ * +normal+
+ * +insert+
+ * +passthrough+
+
+Default: empty
+
[[input.mouse.back_forward_buttons]]
=== input.mouse.back_forward_buttons
Enable back and forward buttons on the mouse.
@@ -3579,9 +3642,9 @@ Default: +pass:[0]+
[[input.spatial_navigation]]
=== input.spatial_navigation
Enable spatial navigation.
-Spatial navigation consists in the ability to navigate between focusable elements in a Web page, such as hyperlinks and form controls, by using Left, Right, Up and Down arrow keys. For example, if the user presses the Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants.
+Spatial navigation consists in the ability to navigate between focusable elements, such as hyperlinks and form controls, on a web page by using the Left, Right, Up and Down arrow keys. For example, if a user presses the Right key, heuristics determine whether there is an element they might be trying to reach towards the right and which element they probably want.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -3717,6 +3780,73 @@ Type: <<types,List of String>>
Default: empty
+[[qt.chromium.low_end_device_mode]]
+=== qt.chromium.low_end_device_mode
+When to use Chromium's low-end device mode.
+This improves the RAM usage of renderer processes, at the expense of performance.
+
+This setting requires a restart.
+
+This setting is only available with the QtWebEngine backend.
+
+Type: <<types,String>>
+
+Valid values:
+
+ * +always+: Always use low-end device mode.
+ * +auto+: Decide automatically (uses low-end mode with < 1 GB available RAM).
+ * +never+: Never use low-end device mode.
+
+Default: +pass:[auto]+
+
+[[qt.chromium.process_model]]
+=== qt.chromium.process_model
+Which Chromium process model to use.
+Alternative process models use less resources, but decrease security and robustness.
+See the following pages for more details:
+
+ - https://www.chromium.org/developers/design-documents/process-models
+ - https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
+
+This setting requires a restart.
+
+This setting is only available with the QtWebEngine backend.
+
+Type: <<types,String>>
+
+Valid values:
+
+ * +process-per-site-instance+: Pages from separate sites are put into separate processes and separate visits to the same site are also isolated.
+ * +process-per-site+: Pages from separate sites are put into separate processes. Unlike Process per Site Instance, all visits to the same site will share an OS process. The benefit of this model is reduced memory consumption, because more web pages will share processes. The drawbacks include reduced security, robustness, and responsiveness.
+ * +single-process+: Run all tabs in a single process. This should be used for debugging purposes only, and it disables `:open --private`.
+
+Default: +pass:[process-per-site-instance]+
+
+[[qt.chromium.sandboxing]]
+=== qt.chromium.sandboxing
+What sandboxing mechanisms in Chromium to use.
+Chromium has various sandboxing layers, which should be enabled for normal browser usage. Mainly for testing and development, it's possible to disable individual sandboxing layers via this setting.
+Open `chrome://sandbox` to see the current sandbox status.
+Changing this setting is only recommended if you know what you're doing, as it **disables one of Chromium's security layers**. To avoid sandboxing being accidentally disabled persistently, this setting can only be set via `config.py`, not via `:set`.
+See the Chromium documentation for more details:
+- https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/linux/sandboxing.md[Linux] - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox.md[Windows] - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox_faq.md[FAQ (Windows-centric)]
+
+This setting requires a restart.
+
+This setting can only be set in config.py.
+
+This setting is only available with the QtWebEngine backend.
+
+Type: <<types,String>>
+
+Valid values:
+
+ * +enable-all+: Enable all available sandboxing mechanisms.
+ * +disable-seccomp-bpf+: Disable the Seccomp BPF filter sandbox (Linux only).
+ * +disable-all+: Disable all sandboxing (**not recommended!**).
+
+Default: +pass:[enable-all]+
+
[[qt.environ]]
=== qt.environ
Additional environment variables to set.
@@ -3782,48 +3912,6 @@ Type: <<types,Bool>>
Default: +pass:[false]+
-[[qt.low_end_device_mode]]
-=== qt.low_end_device_mode
-When to use Chromium's low-end device mode.
-This improves the RAM usage of renderer processes, at the expense of performance.
-
-This setting requires a restart.
-
-This setting is only available with the QtWebEngine backend.
-
-Type: <<types,String>>
-
-Valid values:
-
- * +always+: Always use low-end device mode.
- * +auto+: Decide automatically (uses low-end mode with < 1 GB available RAM).
- * +never+: Never use low-end device mode.
-
-Default: +pass:[auto]+
-
-[[qt.process_model]]
-=== qt.process_model
-Which Chromium process model to use.
-Alternative process models use less resources, but decrease security and robustness.
-See the following pages for more details:
-
- - https://www.chromium.org/developers/design-documents/process-models
- - https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
-
-This setting requires a restart.
-
-This setting is only available with the QtWebEngine backend.
-
-Type: <<types,String>>
-
-Valid values:
-
- * +process-per-site-instance+: Pages from separate sites are put into separate processes and separate visits to the same site are also isolated.
- * +process-per-site+: Pages from separate sites are put into separate processes. Unlike Process per Site Instance, all visits to the same site will share an OS process. The benefit of this model is reduced memory consumption, because more web pages will share processes. The drawbacks include reduced security, robustness, and responsiveness.
- * +single-process+: Run all tabs in a single process. This should be used for debugging purposes only, and it disables `:open --private`.
-
-Default: +pass:[process-per-site-instance]+
-
[[qt.workarounds.locale]]
=== qt.workarounds.locale
Work around locale parsing issues in QtWebEngine 5.15.3.
@@ -3866,7 +3954,7 @@ Default: +pass:[overlay]+
Enable smooth scrolling for web pages.
Note smooth scrolling does not work with the `:scroll-px` command.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
Type: <<types,Bool>>
@@ -4350,6 +4438,7 @@ The following placeholders are defined:
* `{index}`: Index of this tab.
* `{aligned_index}`: Index of this tab padded with spaces to have the same
width.
+* `{relative_index}`: Index of this tab relative to the current tab.
* `{id}`: Internal tab ID of this tab.
* `{scroll_pos}`: Page scroll position.
* `{host}`: Host of the current web page.
@@ -4594,7 +4683,7 @@ Default: +pass:[512]+
=== zoom.text_only
Apply the zoom factor on a frame only to the text or to all content.
-This setting supports URL patterns.
+This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
This setting is only available with the QtWebKit backend.
diff --git a/doc/img/cheatsheet-big.png b/doc/img/cheatsheet-big.png
index 75e2abb89..d47ca913f 100644
--- a/doc/img/cheatsheet-big.png
+++ b/doc/img/cheatsheet-big.png
Binary files differ
diff --git a/doc/img/cheatsheet-small.png b/doc/img/cheatsheet-small.png
index e97d63367..49e5e33df 100644
--- a/doc/img/cheatsheet-small.png
+++ b/doc/img/cheatsheet-small.png
Binary files differ
diff --git a/doc/install.asciidoc b/doc/install.asciidoc
index 83c332b4d..dd284fb9a 100644
--- a/doc/install.asciidoc
+++ b/doc/install.asciidoc
@@ -278,7 +278,7 @@ https://github.com/qutebrowser/qutebrowser/releases[are built] for every
release.
Note that you'll need to upgrade to new versions manually (subscribe to the
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[qutebrowser-announce
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser-announce[qutebrowser-announce
mailinglist] to get notified on new releases). You can install a newer version
without uninstalling the older one.
@@ -335,7 +335,7 @@ files from the
https://github.com/qutebrowser/qutebrowser/releases[release page].
Note that you'll need to upgrade to new versions manually (subscribe to the
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[qutebrowser-announce
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser-announce[qutebrowser-announce
mailinglist] to get notified on new releases).
The binary release ships with a QtWebEngine built without proprietary codec
diff --git a/doc/quickstart.asciidoc b/doc/quickstart.asciidoc
index 2e61e442d..0c42880ab 100644
--- a/doc/quickstart.asciidoc
+++ b/doc/quickstart.asciidoc
@@ -33,8 +33,8 @@ image:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/c
`scripts/asciidoc2html.py` to generate the documentation.
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
* Subscribe to
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist].
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser[the mailinglist] or
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser-announce[the announce-only mailinglist].
* Let me know what features you are missing or things that need (even small!)
improvements.
@@ -52,7 +52,7 @@ or https://matrix.to/#qutebrowser:libera.chat[via Matrix])
* On Reddit: https://www.reddit.com/r/qutebrowser/[/r/qutebrowser]
* Via https://github.com/qutebrowser/qutebrowser/discussions[GitHub Discussions]
* Using the mailinglist: mailto:qutebrowser@lists.qutebrowser.org[]
-(https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[subscribe])
+(https://listi.jpberlin.de/mailman/listinfo/qutebrowser[subscribe])
Donating
--------
diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc
index 8db231add..a2b2bcec9 100644
--- a/doc/qutebrowser.1.asciidoc
+++ b/doc/qutebrowser.1.asciidoc
@@ -34,7 +34,7 @@ show it.
*'URL'*::
URLs to open on startup (empty as a window separator).
-=== optional arguments
+=== options
*-h*, *--help*::
show this help message and exit
@@ -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
@@ -127,7 +130,7 @@ If you found a bug, use the built-in ':report' command to create a bug report
with all information needed.
If you prefer, you can also write to the
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[] instead.
For security bugs, please contact me directly at me@the-compiler.org, GPG ID
@@ -149,9 +152,9 @@ this program. If not, see <https://www.gnu.org/licenses/>.
== RESOURCES
* Website: https://www.qutebrowser.org/
* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] /
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser
* Announce-only mailinglist: mailto:qutebrowser-announce@lists.qutebrowser.org[] /
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce
+https://listi.jpberlin.de/mailman/listinfo/qutebrowser-announce
* IRC: link:ircs://irc.libera.chat:6697/#qutebrowser[`#qutebrowser`] on
https://libera.chat/[Libera Chat] (https://web.libera.chat/#qutebrowser[webchat],
https://matrix.to/#qutebrowser:libera.chat[via Matrix])
diff --git a/doc/userscripts.asciidoc b/doc/userscripts.asciidoc
index 5ff358ee0..4539a0ca8 100644
--- a/doc/userscripts.asciidoc
+++ b/doc/userscripts.asciidoc
@@ -56,6 +56,7 @@ In `command` mode:
- `QUTE_TITLE`: The title of the current page.
- `QUTE_SELECTED_TEXT`: The text currently selected on the page.
- `QUTE_COUNT`: The `count` from the spawn command running the userscript.
+- `QUTE_TAB_INDEX`: The current tab's index.
In `hints` mode:
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 49bc09786..9930514d0 100644
--- a/misc/org.qutebrowser.qutebrowser.appdata.xml
+++ b/misc/org.qutebrowser.qutebrowser.appdata.xml
@@ -44,6 +44,8 @@
</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"/>
<release version="2.2.2" date="2021-05-20"/>
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/qutebrowser.rcc b/misc/qutebrowser.rcc
index 2f73b3726..ffe9e47f6 100644
--- a/misc/qutebrowser.rcc
+++ b/misc/qutebrowser.rcc
@@ -1,13 +1,13 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
- <file>icons/qutebrowser-16x16.png</file>
- <file>icons/qutebrowser-24x24.png</file>
- <file>icons/qutebrowser-32x32.png</file>
- <file>icons/qutebrowser-48x48.png</file>
- <file>icons/qutebrowser-64x64.png</file>
- <file>icons/qutebrowser-96x96.png</file>
- <file>icons/qutebrowser-128x128.png</file>
- <file>icons/qutebrowser-256x256.png</file>
- <file>icons/qutebrowser-512x512.png</file>
+ <file>../icons/qutebrowser-16x16.png</file>
+ <file>../icons/qutebrowser-24x24.png</file>
+ <file>../icons/qutebrowser-32x32.png</file>
+ <file>../icons/qutebrowser-48x48.png</file>
+ <file>../icons/qutebrowser-64x64.png</file>
+ <file>../icons/qutebrowser-96x96.png</file>
+ <file>../icons/qutebrowser-128x128.png</file>
+ <file>../icons/qutebrowser-256x256.png</file>
+ <file>../icons/qutebrowser-512x512.png</file>
</qresource>
</RCC>
diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt
index 8b7067709..63f9eddc5 100644
--- a/misc/requirements/requirements-check-manifest.txt
+++ b/misc/requirements/requirements-check-manifest.txt
@@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-build==0.5.1
-check-manifest==0.46
-packaging==21.0
-pep517==0.10.0
-pyparsing==2.4.7
-toml==0.10.2
+build==0.7.0
+check-manifest==0.48
+packaging==21.3
+pep517==0.12.0
+pyparsing==3.0.7
+tomli==2.0.1
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index 2963888b6..9f7f5074b 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -1,26 +1,43 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
+bleach==4.1.0
+build==0.7.0
bump2version==1.0.1
-certifi==2021.5.30
-cffi==1.14.6
-chardet==4.0.0
-cryptography==3.4.7
-Deprecated==1.2.12
-github3.py==2.0.0
-hunter==3.3.8
-idna==2.10
-jwcrypto==0.9.1
+certifi==2021.10.8
+cffi==1.15.0
+charset-normalizer==2.0.12
+colorama==0.4.4
+cryptography==36.0.2
+docutils==0.18.1
+github3.py==3.2.0
+hunter==3.4.3
+idna==3.3
+importlib-metadata==4.11.3
+jeepney==0.7.1
+keyring==23.5.0
manhole==1.8.0
-packaging==21.0
-pycparser==2.20
-Pympler==0.9
-pyparsing==2.4.7
-PyQt-builder==1.10.3
-python-dateutil==2.8.1
-requests==2.25.1
-sip==6.1.1
+packaging==21.3
+pep517==0.12.0
+pkginfo==1.8.2
+pycparser==2.21
+Pygments==2.11.2
+PyJWT==2.3.0
+Pympler==1.0.1
+pyparsing==3.0.7
+PyQt-builder==1.12.2
+python-dateutil==2.8.2
+readme-renderer==34.0
+requests==2.27.1
+requests-toolbelt==0.9.1
+rfc3986==2.0.0
+SecretStorage==3.3.1
+sip==6.5.1
six==1.16.0
toml==0.10.2
-uritemplate==3.0.1
-# urllib3==1.26.6
-wrapt==1.12.1
+tomli==2.0.1
+tqdm==4.63.1
+twine==3.8.0
+uritemplate==4.1.1
+# urllib3==1.26.9
+webencodings==0.5.1
+zipp==3.7.0
diff --git a/misc/requirements/requirements-dev.txt-raw b/misc/requirements/requirements-dev.txt-raw
index fd840bab1..261f4459f 100644
--- a/misc/requirements/requirements-dev.txt-raw
+++ b/misc/requirements/requirements-dev.txt-raw
@@ -4,6 +4,8 @@ github3.py
bump2version
requests
pyqt-builder
+build
+twine
# Already included via test requirements
#@ ignore: urllib3
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index cc041b538..8a7428933 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -1,24 +1,26 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==21.2.0
-flake8==3.9.2
-flake8-bugbear==21.4.3
+attrs==21.4.0
+flake8==4.0.1
+flake8-bugbear==22.3.23
flake8-builtins==1.5.3
-flake8-comprehensions==3.5.0
+flake8-comprehensions==3.8.0
flake8-copyright==0.2.2
flake8-debugger==4.0.0
flake8-deprecated==1.3
flake8-docstrings==1.6.0
flake8-future-import==0.4.6
flake8-mock==0.3
+flake8-plugin-utils==1.3.2
flake8-polyfill==1.0.2
+flake8-pytest-style==1.6.0
flake8-string-format==0.3.0
-flake8-tidy-imports==4.3.0
+flake8-tidy-imports==4.6.0
flake8-tuple==0.4.1
mccabe==0.6.1
-pep8-naming==0.12.0
-pycodestyle==2.7.0
+pep8-naming==0.12.1
+pycodestyle==2.8.0
pydocstyle==6.1.1
-pyflakes==2.3.1
+pyflakes==2.4.0
six==1.16.0
-snowballstemmer==2.1.0
+snowballstemmer==2.2.0
diff --git a/misc/requirements/requirements-flake8.txt-raw b/misc/requirements/requirements-flake8.txt-raw
index 1bdca6974..59908897c 100644
--- a/misc/requirements/requirements-flake8.txt-raw
+++ b/misc/requirements/requirements-flake8.txt-raw
@@ -11,6 +11,7 @@ flake8-mock
flake8-string-format
flake8-tidy-imports
flake8-tuple
+flake8-pytest-style
pep8-naming
pydocstyle
pyflakes
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index e8b09d628..d8fbba5ee 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -1,21 +1,19 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
chardet==4.0.0
-diff-cover==6.1.1
-importlib-metadata==4.6.1
-importlib-resources==5.2.0
-inflect==5.3.0
-Jinja2==3.0.1
-jinja2-pluralize==0.3.0
-lxml==4.6.3
-MarkupSafe==2.0.1
-mypy==0.910
+diff-cover==6.4.5
+importlib-metadata==4.11.3
+importlib-resources==5.6.0
+Jinja2==3.1.1
+lxml==4.8.0
+MarkupSafe==2.1.1
+mypy==0.942
mypy-extensions==0.4.3
-pluggy==0.13.1
-Pygments==2.9.0
+pluggy==1.0.0
+Pygments==2.11.2
PyQt5-stubs==5.15.2.0
-toml==0.10.2
-types-dataclasses==0.1.5
-types-PyYAML==5.4.3
-typing-extensions==3.10.0.0
-zipp==3.5.0
+tomli==2.0.1
+types-dataclasses==0.6.4
+types-PyYAML==6.0.5
+typing_extensions==4.1.1
+zipp==3.7.0
diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt
deleted file mode 100644
index aba943496..000000000
--- a/misc/requirements/requirements-pip.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-# This file is automatically generated by scripts/dev/recompile_requirements.py
-
-appdirs==1.4.4
-packaging==20.4
-pyparsing==2.4.7
-setuptools==47.3.1
-six==1.15.0
-wheel==0.34.2
diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt
index 86d73655d..d7e46505c 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
-pyinstaller==4.3
-pyinstaller-hooks-contrib==2021.2
+altgraph==0.17.2
+pyinstaller==4.10
+pyinstaller-hooks-contrib==2022.3
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index f0a1c2cb4..3e5ebea9e 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -1,26 +1,30 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-astroid==2.3.3 # rq.filter: < 2.4
-certifi==2021.5.30
-cffi==1.14.6
-chardet==4.0.0
-cryptography==3.4.7
-Deprecated==1.2.12
+astroid==2.11.2
+certifi==2021.10.8
+cffi==1.15.0
+charset-normalizer==2.0.12
+cryptography==36.0.2
+dill==0.3.4
future==0.18.2
-github3.py==2.0.0
-idna==2.10
-isort==4.3.21
-jwcrypto==0.9.1
-lazy-object-proxy==1.4.3
-mccabe==0.6.1
-pefile==2021.5.24
-pycparser==2.20
-pylint==2.4.4 # rq.filter: < 2.5
-python-dateutil==2.8.1
+github3.py==3.2.0
+idna==3.3
+isort==5.10.1
+lazy-object-proxy==1.7.1
+mccabe==0.7.0
+pefile==2021.9.3
+platformdirs==2.5.1 ; python_version>="3.7"
+pycparser==2.21
+PyJWT==2.3.0
+pylint==2.13.3
+python-dateutil==2.8.2
./scripts/dev/pylint_checkers
-requests==2.25.1
+requests==2.27.1
six==1.16.0
-typed-ast==1.4.3 ; python_version<"3.8"
-uritemplate==3.0.1
-# urllib3==1.26.6
-wrapt==1.11.2
+tomli==2.0.1
+typed-ast==1.5.2 ; python_version<"3.8"
+typing_extensions==4.1.1
+uritemplate==4.1.1
+# urllib3==1.26.9
+wrapt==1.14.0
+platformdirs==2.4.0 ; python_version=="3.6.*"
diff --git a/misc/requirements/requirements-pylint.txt-raw b/misc/requirements/requirements-pylint.txt-raw
index 08d340665..52633ec1a 100644
--- a/misc/requirements/requirements-pylint.txt-raw
+++ b/misc/requirements/requirements-pylint.txt-raw
@@ -1,4 +1,4 @@
-pylint<2.5
+pylint
./scripts/dev/pylint_checkers
requests
github3.py
@@ -7,8 +7,10 @@ pefile
# fix qute-pylint location
#@ replace: qute-pylint.* ./scripts/dev/pylint_checkers
#@ markers: typed-ast python_version<"3.8"
-#@ filter: pylint < 2.5
-#@ filter: astroid < 2.4
# Already included via test requirements
#@ ignore: urllib3
+
+# Python 3.6
+#@ markers: platformdirs python_version>="3.7"
+#@ add: platformdirs==2.4.0 ; python_version=="3.6.*"
diff --git a/misc/requirements/requirements-pyqt-5.12.txt b/misc/requirements/requirements-pyqt-5.12.txt
index 890306127..c49484a2d 100644
--- a/misc/requirements/requirements-pyqt-5.12.txt
+++ b/misc/requirements/requirements-pyqt-5.12.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.12.3 # rq.filter: < 5.13
-PyQt5-sip==12.9.0
+PyQt5-sip==12.9.1
PyQtWebEngine==5.12.1 # rq.filter: < 5.13
diff --git a/misc/requirements/requirements-pyqt-5.13.txt b/misc/requirements/requirements-pyqt-5.13.txt
index 5f4da4758..59d78862f 100644
--- a/misc/requirements/requirements-pyqt-5.13.txt
+++ b/misc/requirements/requirements-pyqt-5.13.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.13.2 # rq.filter: < 5.14
-PyQt5-sip==12.9.0
+PyQt5-sip==12.9.1
PyQtWebEngine==5.13.2 # rq.filter: < 5.14
diff --git a/misc/requirements/requirements-pyqt-5.14.txt b/misc/requirements/requirements-pyqt-5.14.txt
index 9ce643666..ac7c0a028 100644
--- a/misc/requirements/requirements-pyqt-5.14.txt
+++ b/misc/requirements/requirements-pyqt-5.14.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.14.2 # rq.filter: < 5.15
-PyQt5-sip==12.9.0
+PyQt5-sip==12.9.1
PyQtWebEngine==5.14.0 # rq.filter: < 5.15
diff --git a/misc/requirements/requirements-pyqt-5.15.0.txt b/misc/requirements/requirements-pyqt-5.15.0.txt
index b111a93f3..6164b89ca 100644
--- a/misc/requirements/requirements-pyqt-5.15.0.txt
+++ b/misc/requirements/requirements-pyqt-5.15.0.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.0 # rq.filter: == 5.15.0
-PyQt5-sip==12.9.0
+PyQt5-sip==12.9.1
PyQtWebEngine==5.15.0 # rq.filter: == 5.15.0
diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt
index 8b7a53c44..03f016177 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
+PyQt5-sip==12.9.1
+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..bf667ac97 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
+PyQt5-sip==12.9.1
+PyQtWebEngine==5.15.5
PyQtWebEngine-Qt5==5.15.2
diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt
index a83804b9e..a78ba8560 100644
--- a/misc/requirements/requirements-pyroma.txt
+++ b/misc/requirements/requirements-pyroma.txt
@@ -1,10 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-certifi==2021.5.30
-chardet==4.0.0
-docutils==0.17.1
-idna==2.10
-Pygments==2.9.0
-pyroma==3.2
-requests==2.25.1
-urllib3==1.26.6
+certifi==2021.10.8
+charset-normalizer==2.0.12
+docutils==0.18.1
+idna==3.3
+Pygments==2.11.2
+pyroma==3.3
+requests==2.27.1
+urllib3==1.26.9
diff --git a/misc/requirements/requirements-qutebrowser.txt-raw b/misc/requirements/requirements-qutebrowser.txt-raw
index e07ea992a..b260fa16c 100644
--- a/misc/requirements/requirements-qutebrowser.txt-raw
+++ b/misc/requirements/requirements-qutebrowser.txt-raw
@@ -14,7 +14,18 @@ adblock # Improved adblocking
importlib-metadata # Determining PyQt version
typing_extensions # from importlib-metadata
-#@ markers: importlib-resources python_version<"3.9"
-#@ markers: importlib-metadata python_version<"3.8"
+#@ markers: importlib-resources python_version=="3.7.*" or python_version=="3.8.*"
+#@ markers: importlib-metadata python_version=="3.7.*"
#@ markers: typing_extensions python_version<"3.8"
#@ markers: dataclasses python_version<"3.7"
+
+# Python 3.6
+#@ add: importlib-resources<5.6.0 ; python_version=="3.6.*"
+#@ add: importlib-metadata<4.9 ; python_version=="3.6.*"
+#
+#@ markers: zipp python_version>="3.7"
+#@ add: zipp<3.7 ; python_version=="3.6.*"
+#@ markers: MarkupSafe python_version>="3.7"
+#@ add: MarkupSafe<2.1.0 ; python_version=="3.6.*"
+#@ markers: Jinja2 python_version>="3.7"
+#@ add: Jinja2<3.1.0 ; python_version=="3.6.*"
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index f5a7cb3ca..bec429e04 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -2,24 +2,26 @@
alabaster==0.7.12
Babel==2.9.1
-certifi==2021.5.30
-chardet==4.0.0
+certifi==2021.10.8
+charset-normalizer==2.0.12
docutils==0.17.1
-idna==2.10
-imagesize==1.2.0
-Jinja2==3.0.1
-MarkupSafe==2.0.1
-packaging==21.0
-Pygments==2.9.0
-pyparsing==2.4.7
-pytz==2021.1
-requests==2.25.1
-snowballstemmer==2.1.0
-Sphinx==4.1.0
+idna==3.3
+imagesize==1.3.0
+importlib-metadata==4.11.3
+Jinja2==3.1.1
+MarkupSafe==2.1.1
+packaging==21.3
+Pygments==2.11.2
+pyparsing==3.0.7
+pytz==2022.1
+requests==2.27.1
+snowballstemmer==2.2.0
+Sphinx==4.5.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
-urllib3==1.26.6
+urllib3==1.26.9
+zipp==3.7.0
diff --git a/misc/requirements/requirements-tests-bleeding.txt b/misc/requirements/requirements-tests-bleeding.txt
index 5fbb05f05..72d6ad083 100644
--- a/misc/requirements/requirements-tests-bleeding.txt
+++ b/misc/requirements/requirements-tests-bleeding.txt
@@ -7,7 +7,9 @@ git+https://github.com/pallets/flask.git
git+https://github.com/pallets/werkzeug.git # transitive dep, but needed to work
git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-python
git+https://github.com/pytest-dev/pytest.git
-git+https://github.com/pytest-dev/pytest-bdd.git
+# Problematic: https://github.com/pytest-dev/pytest-bdd/issues/447
+# git+https://github.com/pytest-dev/pytest-bdd.git
+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
@@ -32,6 +34,7 @@ adblock
## qutebrowser dependencies
git+https://github.com/pallets/jinja.git
-git+https://github.com/yaml/pyyaml.git
+# Problematic: https://github.com/yaml/pyyaml/issues/601
+PyYAML
git+https://github.com/tartley/colorama.git
git+https://github.com/pyparsing/pyparsing.git
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 0d4db4579..787d2791b 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -1,59 +1,76 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==21.2.0
-beautifulsoup4==4.9.3
-certifi==2021.5.30
-chardet==4.0.0
-cheroot==8.5.2
-click==8.0.1
-coverage==5.5
-EasyProcess==0.3
+attrs==21.4.0
+beautifulsoup4==4.10.0
+certifi==2021.10.8
+charset-normalizer==2.0.12
+cheroot==8.6.0
+click==8.1.0 ; python_version>="3.7"
+coverage==6.3.2 ; python_version>="3.7"
execnet==1.9.0
-filelock==3.0.12
-Flask==2.0.1
+filelock==3.6.0 ; python_version>="3.7"
+Flask==2.1.0 ; python_version>="3.7"
glob2==0.7
-hunter==3.3.8
-hypothesis==6.14.2
-icdiff==1.9.1
-idna==2.10
+hunter==3.4.3
+hypothesis==6.40.0 ; python_version>="3.7"
+icdiff==2.0.4
+idna==3.3
+importlib-metadata==4.11.3 ; python_version=="3.7.*"
iniconfig==1.1.1
-itsdangerous==2.0.1
-jaraco.functools==3.3.0
-# Jinja2==3.0.1
-Mako==1.1.4
+itsdangerous==2.1.2 ; python_version>="3.7"
+jaraco.functools==3.5.0 ; python_version>="3.7"
+# Jinja2==3.1.1
+Mako==1.2.0 ; python_version>="3.7"
manhole==1.8.0
-# MarkupSafe==2.0.1
-more-itertools==8.8.0
-packaging==21.0
+# MarkupSafe==2.1.1
+more-itertools==8.12.0
+packaging==21.3
parse==1.19.0
-parse-type==0.5.2
-pluggy==0.13.1
+parse-type==0.6.0
+pluggy==1.0.0
pprintpp==0.4.0
-py==1.10.0
+py==1.11.0
py-cpuinfo==8.0.0
-Pygments==2.9.0
-pyparsing==2.4.7
-pytest==6.2.4
+Pygments==2.11.2
+pyparsing==3.0.7
+pytest==7.1.1 ; python_version>="3.7"
pytest-bdd==4.1.0
pytest-benchmark==3.4.1
-pytest-cov==2.12.1
-pytest-forked==1.3.0
+pytest-cov==3.0.0
+pytest-forked==1.4.0
pytest-icdiff==0.5
pytest-instafail==0.4.2
-pytest-mock==3.6.1
+pytest-mock==3.7.0 ; python_version>="3.7"
pytest-qt==4.0.2
pytest-repeat==0.9.1
-pytest-rerunfailures==10.1
-pytest-xdist==2.3.0
+pytest-rerunfailures==10.2
+pytest-xdist==2.5.0
pytest-xvfb==2.0.0
-PyVirtualDisplay==2.2
-requests==2.25.1
+PyVirtualDisplay==3.0
+requests==2.27.1
requests-file==1.5.1
six==1.16.0
sortedcontainers==2.4.0
-soupsieve==2.2.1
-tldextract==3.1.0
+soupsieve==2.3.1
+tldextract==3.2.0 ; python_version>="3.7"
toml==0.10.2
-urllib3==1.26.6
+tomli==2.0.1 ; python_version>="3.7"
+urllib3==1.26.9
vulture==2.3
-Werkzeug==2.0.1
+Werkzeug==2.1.0 ; python_version>="3.7"
+zipp==3.7.0 ; python_version>="3.7"
+jaraco.functools<3.5 ; python_version=="3.6.*"
+tomli<2 ; python_version=="3.6.*"
+filelock==3.4.1 ; python_version=="3.6.*"
+hypothesis<6.32 ; python_version=="3.6.*"
+coverage<6.3 ; python_version=="3.6.*"
+pytest-mock<3.7 ; python_version=="3.6.*"
+itsdangerous<2.1.0 ; python_version=="3.6.*"
+tldextract<3.2.0 ; python_version=="3.6.*"
+Mako<1.2.0 ; python_version=="3.6.*"
+pytest<7.1.0 ; python_version=="3.6.*"
+click<8.1.0 ; python_version=="3.6.*"
+Flask<2.1.0 ; python_version=="3.6.*"
+Werkzeug<2.1.0 ; python_version=="3.6.*"
+zipp<3.7 ; python_version=="3.6.*"
+importlib-metadata<4.9 ; python_version=="3.6.*"
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index ab580ac4b..6338a1a97 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
@@ -34,3 +35,35 @@ pytest-icdiff
tldextract
#@ ignore: Jinja2, MarkupSafe, colorama
+
+# Python 3.6
+#@ markers: jaraco.functools python_version>="3.7"
+#@ add: jaraco.functools<3.5 ; python_version=="3.6.*"
+#@ markers: tomli python_version>="3.7"
+#@ add: tomli<2 ; python_version=="3.6.*"
+#@ markers: filelock python_version>="3.7"
+#@ add: filelock==3.4.1 ; python_version=="3.6.*"
+#@ markers: hypothesis python_version>="3.7"
+#@ add: hypothesis<6.32 ; python_version=="3.6.*"
+#@ markers: coverage python_version>="3.7"
+#@ add: coverage<6.3 ; python_version=="3.6.*"
+#@ markers: pytest-mock python_version>="3.7"
+#@ add: pytest-mock<3.7 ; python_version=="3.6.*"
+#@ markers: itsdangerous python_version>="3.7"
+#@ add: itsdangerous<2.1.0 ; python_version=="3.6.*"
+#@ markers: tldextract python_version>="3.7"
+#@ add: tldextract<3.2.0 ; python_version=="3.6.*"
+#@ markers: Mako python_version>="3.7"
+#@ add: Mako<1.2.0 ; python_version=="3.6.*"
+#@ markers: pytest python_version>="3.7"
+#@ add: pytest<7.1.0 ; python_version=="3.6.*"
+#@ markers: click python_version>="3.7"
+#@ add: click<8.1.0 ; python_version=="3.6.*"
+#@ markers: Flask python_version>="3.7"
+#@ add: Flask<2.1.0 ; python_version=="3.6.*"
+#@ markers: Werkzeug python_version>="3.7"
+#@ add: Werkzeug<2.1.0 ; python_version=="3.6.*"
+#@ markers: zipp python_version>="3.7"
+#@ add: zipp<3.7 ; python_version=="3.6.*"
+#@ markers: importlib-metadata python_version=="3.7.*"
+#@ add: importlib-metadata<4.9 ; python_version=="3.6.*"
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 82eb5b25c..a87519740 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -1,16 +1,20 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-appdirs==1.4.4
-distlib==0.3.2
-filelock==3.0.12
-packaging==21.0
-pip==21.1.3
-pluggy==0.13.1
-py==1.10.0
-pyparsing==2.4.7
-setuptools==57.1.0
+distlib==0.3.4
+filelock==3.6.0 ; python_version>="3.7"
+packaging==21.3
+pip==22.0.4 ; python_version>="3.7"
+platformdirs==2.5.1 ; python_version>="3.7"
+pluggy==1.0.0
+py==1.11.0
+pyparsing==3.0.7
+setuptools==61.2.0 ; python_version>="3.7"
six==1.16.0
toml==0.10.2
-tox==3.23.1
-virtualenv==20.4.7
-wheel==0.36.2
+tox==3.24.5
+virtualenv==20.14.0
+wheel==0.37.1
+setuptools<60 ; python_version=="3.6.*"
+filelock==3.4.1 ; python_version=="3.6.*"
+platformdirs==2.4.0 ; python_version=="3.6.*"
+pip==21.3.1 ; python_version=="3.6.*"
diff --git a/misc/requirements/requirements-tox.txt-raw b/misc/requirements/requirements-tox.txt-raw
index 27d58e1f4..2a9f30c5a 100644
--- a/misc/requirements/requirements-tox.txt-raw
+++ b/misc/requirements/requirements-tox.txt-raw
@@ -1,2 +1,12 @@
tox
wheel
+
+# Python 3.6
+#@ markers: setuptools python_version>="3.7"
+#@ add: setuptools<60 ; python_version=="3.6.*"
+#@ markers: filelock python_version>="3.7"
+#@ add: filelock==3.4.1 ; python_version=="3.6.*"
+#@ markers: platformdirs python_version>="3.7"
+#@ add: platformdirs==2.4.0 ; python_version=="3.6.*"
+#@ markers: pip python_version>="3.7"
+#@ add: pip==21.3.1 ; python_version=="3.6.*"
diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt
index a911b9d2b..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.8.1
-PyYAML==5.4.1
-yamllint==1.26.1
+pathspec==0.9.0
+PyYAML==6.0
+yamllint==1.26.3
diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md
index f5325127b..8d65d83fd 100644
--- a/misc/userscripts/README.md
+++ b/misc/userscripts/README.md
@@ -83,6 +83,15 @@ The following userscripts can be found on their own repositories.
services (Invidious, Teddit, Nitter, Bibliogram, OpenStreetMap).
- [CIAvash/qutebrowser-userscripts](https://github.com/CIAvash/qutebrowser-userscripts),
various small userscripts written in Raku.
+- [bitwarden-rofi](https://github.com/haztecaso/bitwarden-rofi), rofi wrapper for bitwarden cli
+ interface using gpg instead of keyctl.
+- [yomichad](https://github.com/potamides/yomichad): Japanese pop-up dictionary
+ for looking up readings and definitions of currently selected words, kanji
+ and names
+- [~palb91/qutescripts](https://git.sr.ht/~palb91/qutescripts): A list of
+ personal userscripts for qutebrowser (`domcycle`: settings per domain,
+ `gitclone`, `jsdownload`: smart download, and `substiqute`: bash-like url
+ substitution)
[Zotero]: https://www.zotero.org/
[Pocket]: https://getpocket.com/
diff --git a/misc/userscripts/cast b/misc/userscripts/cast
index df74fe97e..ec703d5fb 100755
--- a/misc/userscripts/cast
+++ b/misc/userscripts/cast
@@ -20,6 +20,14 @@
#
# Dependencies
# - castnow, https://github.com/xat/castnow
+# - youtube-dl (https://youtube-dl.org/) or a drop-in replacement such as
+# yt-dlp (https://github.com/yt-dlp/yt-dlp).
+#
+# Configuration:
+# This script looks at the optional QUTE_CAST_YTDL_PROGRAM environment
+# variable (if it exists) to decide which program to use for downloading
+# videos. If specified, this should be youtube-dl or a drop-in replacement
+# for it.
#
# Author
# Simon Désaulniers <sim.desaulniers@gmail.com>
@@ -133,23 +141,34 @@ echo "jseval -q $(printjs)" >> "$QUTE_FIFO"
tmpdir=$(mktemp -d)
file_to_cast=${tmpdir}/qutecast
-program_=$(command -v castnow)
+cast_program=$(command -v castnow)
+
+# pick a ytdl program
+for p in "$QUTE_CAST_YTDL_PROGRAM" yt-dlp youtube-dl; do
+ ytdl_program=$(command -v -- "$p")
+ [ "$ytdl_program" == "" ] || break
+done
-if [[ "${program_}" == "" ]]; then
- msg error "castnow can't be found..."
+if [[ "${cast_program}" == "" ]]; then
+ msg error "castnow can't be found"
+ exit 1
+fi
+if [[ "${ytdl_program}" == "" ]]; then
+ msg error "youtube-dl or a drop-in replacement can't be found in PATH, and no installed program " \
+ "specified in QUTE_CAST_YTDL_PROGRAM (currently \"$QUTE_CAST_YTDL_PROGRAM\")"
exit 1
fi
# kill any running instance of castnow
-pkill -f "${program_}"
+pkill -f -- "${cast_program}"
# start youtube download in stream mode (-o -) into temporary file
-youtube-dl -qo - "$1" > "${file_to_cast}" &
+"${ytdl_program}" -qo - "$1" > "${file_to_cast}" &
ytdl_pid=$!
msg info "Casting $1" >> "$QUTE_FIFO"
# start castnow in stream mode to cast on ChromeCast
-tail -F "${file_to_cast}" | ${program_} -
+tail -F "${file_to_cast}" | ${cast_program} -
# cleanup remaining background process and file on disk
kill ${ytdl_pid}
diff --git a/misc/userscripts/open_download b/misc/userscripts/open_download
index 62730f37c..e118765f7 100755
--- a/misc/userscripts/open_download
+++ b/misc/userscripts/open_download
@@ -103,13 +103,22 @@ fi
file="${entries[$line]}"
file="${file%%$'\t'*}"
path="$DOWNLOAD_DIR/$file"
-filetype=$(xdg-mime query filetype "$path")
-application=$(xdg-mime query default "$filetype")
-if [ -z "$application" ] ; then
- die "Do not know how to open »$file« of type $filetype"
-fi
+if [ -f /.flatpak-info ]; then
+ # with the help of the appchooser portal, flatpak let the user select the
+ # app associated with a mime type after executing xdg-open.
+ # we can't know ahead of time which app will be launched, and the sandbox
+ # doesn't have access to mime types known to the host.
+ msg info "Opening »$file« with XDG Desktop Portal"
+else
+ filetype=$(xdg-mime query filetype "$path")
+ application=$(xdg-mime query default "$filetype")
+
+ if [ -z "$application" ] ; then
+ die "Do not know how to open »$file« of type $filetype"
+ fi
-msg info "Opening »$file« (of type $filetype) with ${application%.desktop}"
+ msg info "Opening »$file« (of type $filetype) with ${application%.desktop}"
+fi
-xdg-open "$path" &
+xdg-open "$path"
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/misc/userscripts/qute-bitwarden b/misc/userscripts/qute-bitwarden
index c6407127a..f8629d1dc 100755
--- a/misc/userscripts/qute-bitwarden
+++ b/misc/userscripts/qute-bitwarden
@@ -6,7 +6,7 @@
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published bjy
+# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
@@ -62,6 +62,8 @@ argument_parser = argparse.ArgumentParser(
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
argument_parser.add_argument('--dmenu-invocation', '-d', default='rofi -dmenu -i -p Bitwarden',
help='Invocation used to execute a dmenu-provider')
+argument_parser.add_argument('--password-prompt-invocation', '-p', default='rofi -dmenu -p "Master Password" -password -lines 0',
+ help='Invocation used to prompt the user for their Bitwarden password')
argument_parser.add_argument('--no-insert-mode', '-n', dest='insert_mode', action='store_false',
help="Don't automatically enter insert mode")
argument_parser.add_argument('--totp', '-t', action='store_true',
@@ -98,16 +100,12 @@ def qute_command(command):
fifo.flush()
-def ask_password():
- process = subprocess.run([
- 'rofi',
- '-dmenu',
- '-p',
- 'Master Password',
- '-password',
- '-lines',
- '0',
- ], universal_newlines=True, stdout=subprocess.PIPE)
+def ask_password(password_prompt_invocation):
+ process = subprocess.run(
+ shlex.split(password_prompt_invocation),
+ universal_newlines=True,
+ stdout=subprocess.PIPE,
+ )
if process.returncode > 0:
raise Exception('Could not unlock vault')
master_pass = process.stdout.strip()
@@ -117,10 +115,10 @@ def ask_password():
).strip()
-def get_session_key(auto_lock):
+def get_session_key(auto_lock, password_prompt_invocation):
if auto_lock == 0:
subprocess.call(['keyctl', 'purge', 'user', 'bw_session'])
- return ask_password()
+ return ask_password(password_prompt_invocation)
else:
process = subprocess.run(
['keyctl', 'request', 'user', 'bw_session'],
@@ -129,7 +127,7 @@ def get_session_key(auto_lock):
)
key_id = process.stdout.strip()
if process.returncode > 0:
- session = ask_password()
+ session = ask_password(password_prompt_invocation)
if not session:
raise Exception('Could not unlock vault')
key_id = subprocess.check_output(
@@ -145,8 +143,8 @@ def get_session_key(auto_lock):
).strip()
-def pass_(domain, encoding, auto_lock):
- session_key = get_session_key(auto_lock)
+def pass_(domain, encoding, auto_lock, password_prompt_invocation):
+ session_key = get_session_key(auto_lock, password_prompt_invocation)
process = subprocess.run(
['bw', 'list', 'items', '--session', session_key, '--url', domain],
stdout=subprocess.PIPE,
@@ -166,8 +164,8 @@ def pass_(domain, encoding, auto_lock):
return out
-def get_totp_code(selection_id, domain_name, encoding, auto_lock):
- session_key = get_session_key(auto_lock)
+def get_totp_code(selection_id, domain_name, encoding, auto_lock, password_prompt_invocation):
+ session_key = get_session_key(auto_lock, password_prompt_invocation)
process = subprocess.run(
['bw', 'get', 'totp', '--session', session_key, selection_id],
stdout=subprocess.PIPE,
@@ -224,6 +222,7 @@ def main(arguments):
target,
arguments.io_encoding,
arguments.auto_lock,
+ arguments.password_prompt_invocation,
)
)
if not target_candidates:
@@ -270,7 +269,8 @@ def main(arguments):
selection['id'],
selection['name'],
arguments.io_encoding,
- arguments.auto_lock
+ arguments.auto_lock,
+ arguments.password_prompt_invocation,
)
)
else:
@@ -294,7 +294,8 @@ def main(arguments):
selection['id'],
selection['name'],
arguments.io_encoding,
- arguments.auto_lock
+ arguments.auto_lock,
+ arguments.password_prompt_invocation,
)
)
diff --git a/misc/userscripts/qute-keepassxc b/misc/userscripts/qute-keepassxc
index 11d0a3384..f4e971d3c 100755
--- a/misc/userscripts/qute-keepassxc
+++ b/misc/userscripts/qute-keepassxc
@@ -62,7 +62,7 @@ Unlike most browser extensions which only use plain local storage, this one atte
by storing the key in encrypted form using GPG.
Therefore you need to have a public-key-pair readily set up.
-GPG might then ask for your private-key passwort whenever you query the database for login credentials.
+GPG might then ask for your private-key password whenever you query the database for login credentials.
[1]: https://keepassxc.org/
diff --git a/misc/userscripts/qute-lastpass b/misc/userscripts/qute-lastpass
index d2a72f077..edb48dfdc 100755
--- a/misc/userscripts/qute-lastpass
+++ b/misc/userscripts/qute-lastpass
@@ -6,7 +6,7 @@
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published bjy
+# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
diff --git a/misc/userscripts/qute-pass b/misc/userscripts/qute-pass
index 71646eb69..f410802c8 100755..100644
--- a/misc/userscripts/qute-pass
+++ b/misc/userscripts/qute-pass
@@ -23,11 +23,12 @@ demonstration can be seen here: https://i.imgur.com/KN3XuZP.gif.
"""
USAGE = """The domain of the site has to appear as a segment in the pass path,
-for example: "github.com/cryzed" or "websites/github.com". How the username and
-password are determined is freely configurable using the CLI arguments. As an
-example, if you instead store the username as part of the secret (and use a
-site's name as filename), instead of the default configuration, use
-`--username-target secret` and `--username-pattern "username: (.+)"`.
+for example: "github.com/cryzed" or "websites/github.com". Alternatively the
+parameter `--unfiltered` may be used to get a list of all passwords. How the
+username and password are determined is freely configurable using the CLI
+arguments. As an example, if you instead store the username as part of the
+secret (and use a site's name as filename), instead of the default configuration,
+use `--username-target secret` and `--username-pattern "username: (.+)"`.
The login information is inserted by emulating key events using qutebrowser's
fake-key command in this manner: [USERNAME]<Tab>[PASSWORD], which is compatible
@@ -76,6 +77,8 @@ argument_parser.add_argument('--password-store', '-p',
help='Path to your pass password-store (only used in pass-mode)', type=expanded_path)
argument_parser.add_argument('--mode', '-M', choices=['pass', 'gopass'], default="pass",
help='Select mode [gopass] to use gopass instead of the standard pass.')
+argument_parser.add_argument('--prefix', type=str,
+ help='Search only the given subfolder of the store (only used in gopass-mode)')
argument_parser.add_argument('--username-pattern', '-u', default=r'.*/(.+)',
help='Regular expression that matches the username')
argument_parser.add_argument('--username-target', '-U', choices=['path', 'secret'], default='path',
@@ -92,6 +95,10 @@ argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
help='Merge pass candidates for fully-qualified and registered domain name')
argument_parser.add_argument('--extra-url-suffixes', '-s', default='',
help='Comma-separated string containing extra suffixes (e.g local)')
+argument_parser.add_argument('--unfiltered', dest='unfiltered', action='store_true',
+ help='Show an unfiltered selection of all passwords in the store')
+argument_parser.add_argument('--always-show-selection', dest='always_show_selection', action='store_true',
+ help='Always show selection, even if there is only a single match')
group = argument_parser.add_mutually_exclusive_group()
group.add_argument('--username-only', '-e', action='store_true', help='Only insert username')
group.add_argument('--password-only', '-w', action='store_true', help='Only insert password')
@@ -123,14 +130,17 @@ def qute_command(command):
fifo.flush()
-def find_pass_candidates(domain):
+def find_pass_candidates(domain, unfiltered=False):
candidates = []
if arguments.mode == "gopass":
- all_passwords = subprocess.run(["gopass", "list", "--flat" ], stdout=subprocess.PIPE).stdout.decode("UTF-8").splitlines()
+ gopass_args = ["gopass", "list", "--flat"]
+ if arguments.prefix:
+ gopass_args.append(arguments.prefix)
+ all_passwords = subprocess.run(gopass_args, stdout=subprocess.PIPE).stdout.decode("UTF-8").splitlines()
for password in all_passwords:
- if domain in password:
+ if unfiltered or domain in password:
candidates.append(password)
else:
for path, directories, file_names in os.walk(arguments.password_store, followlinks=True):
@@ -143,7 +153,7 @@ def find_pass_candidates(domain):
split_path = pass_path.split(os.path.sep)
for secret in secrets:
secret_base = os.path.splitext(secret)[0]
- if domain not in (split_path + [secret_base]):
+ if not unfiltered and domain not in (split_path + [secret_base]):
continue
candidates.append(os.path.join(pass_path, secret_base))
@@ -223,7 +233,7 @@ def main(arguments):
for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.ipv4, private_domain]):
attempted_targets.append(target)
- target_candidates = find_pass_candidates(target)
+ target_candidates = find_pass_candidates(target, unfiltered=arguments.unfiltered)
if not target_candidates:
continue
@@ -235,7 +245,11 @@ def main(arguments):
stderr('No pass candidates for URL {!r} found! (I tried {!r})'.format(arguments.url, attempted_targets))
return ExitCodes.NO_PASS_CANDIDATES
- selection = candidates.pop() if len(candidates) == 1 else dmenu(sorted(candidates), arguments.dmenu_invocation)
+ if len(candidates) == 1 and not arguments.always_show_selection:
+ selection = candidates.pop()
+ else:
+ selection = dmenu(sorted(candidates), arguments.dmenu_invocation)
+
# Nothing was selected, simply return
if not selection:
return ExitCodes.SUCCESS
diff --git a/misc/userscripts/ripbang b/misc/userscripts/ripbang
index df9cb2fbc..2f867c838 100755
--- a/misc/userscripts/ripbang
+++ b/misc/userscripts/ripbang
@@ -9,26 +9,22 @@
# :spawn --userscript ripbang amazon maps
#
-from __future__ import print_function
import os, re, requests, sys
-
-try:
- from urllib.parse import unquote
-except ImportError:
- from urllib import unquote
+from urllib.parse import urlparse, parse_qs
for argument in sys.argv[1:]:
bang = '!' + argument
r = requests.get('https://duckduckgo.com/',
- params={'q': bang + ' SEARCHTEXT'})
+ params={'q': bang + ' SEARCHTEXT'},
+ headers={'user-agent': 'qutebrowser ripbang'})
- searchengine = unquote(re.search("url=[^']+", r.text).group(0))
- searchengine = searchengine.replace('url=', '')
- searchengine = searchengine.replace('/l/?kh=-1&uddg=', '')
+ searchengine = re.search("url=([^']+)", r.text).group(1)
+ searchengine = urlparse(searchengine).query
+ searchengine = parse_qs(searchengine)['uddg'][0]
searchengine = searchengine.replace('SEARCHTEXT', '{}')
if os.getenv('QUTE_FIFO'):
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
- fifo.write('set searchengines %s %s' % (bang, searchengine))
+ fifo.write('config-dict-add url.searchengines %s %s' % (bang, searchengine))
else:
print('%s %s' % (bang, searchengine))
diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py
index d146eb8bf..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.0"
+__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/browsertab.py b/qutebrowser/browser/browsertab.py
index b1827dbf4..661c5f68b 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -779,6 +779,7 @@ class AbstractAudio(QObject):
"""Set this tab as muted or not.
Arguments:
+ muted: Whether the tab is currently muted.
override: If set to True, muting/unmuting was done manually and
overrides future automatic mute/unmute changes based on
the URL.
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 8cd73ae4f..00d5e521f 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -383,17 +383,18 @@ class CommandDispatcher:
yield parsed
@cmdutils.register(instance='command-dispatcher', scope='window')
- def tab_clone(self, bg=False, window=False):
+ def tab_clone(self, bg=False, window=False, private=False):
"""Duplicate the current tab.
Args:
bg: Open in a background tab.
window: Open in a new window.
+ private: Open in a new private window.
Return:
The new QWebView.
"""
- cmdutils.check_exclusive((bg, window), 'bw')
+ cmdutils.check_exclusive((bg, window, private), 'bwp')
curtab = self._current_widget()
cur_title = self._tabbed_browser.widget.page_title(
self._current_index())
@@ -404,9 +405,9 @@ class CommandDispatcher:
# The new tab could be in a new tabbed_browser (e.g. because of
# tabs.tabs_are_windows being set)
- if window:
+ if window or private:
new_tabbed_browser = self._new_tabbed_browser(
- private=self._tabbed_browser.is_private)
+ private=self._tabbed_browser.is_private or private)
else:
new_tabbed_browser = self._tabbed_browser
newtab = new_tabbed_browser.tabopen(background=bg)
@@ -450,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)
@@ -499,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."""
@@ -1003,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.
@@ -1016,24 +1017,30 @@ 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:
+ # pylint: disable=else-if-used
# 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)
@@ -1145,6 +1152,7 @@ class CommandDispatcher:
idx = self._current_index()
if idx != -1:
+ env['QUTE_TAB_INDEX'] = str(idx + 1)
env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)
# FIXME:qtwebengine: If tab is None, run_async will fail!
@@ -1514,6 +1522,7 @@ class CommandDispatcher:
Callback for GUIProcess when the edited text was updated.
Args:
+ ed: The editor.ExternalEditor instance
elem: The WebElementWrapper which was modified.
text: The new text to insert.
"""
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index fa4ac9da2..32bfd2693 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -611,7 +611,11 @@ class AbstractDownloadItem(QObject):
message.error(str(e))
def url(self) -> QUrl:
- """Get the download's origin URL."""
+ """Get the download's URL (i.e. where the file is downloaded from)."""
+ raise NotImplementedError
+
+ def origin(self) -> QUrl:
+ """Get the download's origin URL (i.e. the page starting the download)."""
raise NotImplementedError
def _get_open_filename(self):
@@ -819,6 +823,34 @@ class AbstractDownloadItem(QObject):
self.pdfjs_requested.emit(os.path.basename(filename),
self.url())
+ def cancel_for_origin(self) -> bool:
+ """Cancel the download based on URL/origin.
+
+ For some special cases, we want to cancel downloads immediately, before
+ downloading:
+
+ - file:// downloads from file:// URLs (open the file instead)
+ - http:// downloads from https:// URLs (mixed content)
+ """
+ origin = self.origin()
+ url = self.url()
+ if not origin.isValid():
+ return False
+
+ if url.scheme() == "file" and origin.scheme() == "file":
+ utils.open_file(url.toLocalFile())
+ self.cancel()
+ return True
+
+ if (url.scheme() == "http" and
+ origin.isValid() and origin.scheme() == "https" and
+ config.instance.get("downloads.prevent_mixed_content", url=origin)):
+ self._die("Aborting insecure download from secure page "
+ "(see downloads.prevent_mixed_content).")
+ return True
+
+ return False
+
def set_target(self, target):
"""Set the target for a given download.
@@ -1320,6 +1352,7 @@ class TempDownloadManager:
The tempfile.TemporaryDirectory that is used.
"""
if self._tmpdir is None:
+ # pylint: disable=consider-using-with
self._tmpdir = tempfile.TemporaryDirectory(
prefix='qutebrowser-downloads-')
return self._tmpdir
@@ -1341,6 +1374,7 @@ class TempDownloadManager:
suggested_name = utils.sanitize_filename(suggested_name)
# Make sure that the filename is not too long
suggested_name = utils.elide_filename(suggested_name, 50)
+ # pylint: disable=consider-using-with
fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False,
suffix='_' + suggested_name)
self.files.append(fobj)
diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py
index d0245937f..5abb9a137 100644
--- a/qutebrowser/browser/greasemonkey.py
+++ b/qutebrowser/browser/greasemonkey.py
@@ -324,6 +324,7 @@ class GreasemonkeyManager(QObject):
"""Add a GreasemonkeyScript to this manager.
Args:
+ script: The GreasemonkeyScript to add.
force: Fetch and overwrite any dependencies which are
already locally cached.
"""
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index e127cd10a..2e4e8e4b4 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -299,7 +299,7 @@ class HintActions:
Args:
elem: The QWebElement to download.
- _context: The HintContext to use.
+ context: The HintContext to use.
"""
url = elem.resolve_url(context.baseurl)
if url is None:
@@ -596,6 +596,7 @@ class HintManager(QObject):
"'args' is required with target userscript/spawn/run/"
"fill.")
else:
+ # pylint: disable=else-if-used
if args:
raise cmdutils.CommandError(
"'args' is only allowed with target userscript/spawn.")
@@ -684,9 +685,8 @@ class HintManager(QObject):
Args:
rapid: Whether to do rapid hinting. With rapid hinting, the hint
mode isn't left after a hint is followed, so you can easily
- open multiple links. This is only possible with targets
- `tab` (with `tabs.background=true`), `tab-bg`,
- `window`, `run`, `hover`, `userscript` and `spawn`.
+ open multiple links. Note this won't work with targets
+ `tab-fg`, `fill`, `delete` and `right-click`.
add_history: Whether to add the spawned or yanked link to the
browsing history.
first: Click the first hinted element without prompting.
@@ -754,18 +754,16 @@ class HintManager(QObject):
if mode_manager.mode == usertypes.KeyMode.hint:
modeman.leave(self._win_id, usertypes.KeyMode.hint, 're-hinting')
- if rapid:
- if target in [Target.tab_bg, Target.window, Target.run,
- Target.hover, Target.userscript, Target.spawn,
- Target.download, Target.normal, Target.current,
- Target.yank, Target.yank_primary]:
- pass
- elif target == Target.tab and config.val.tabs.background:
- pass
- else:
- name = target.name.replace('_', '-')
- raise cmdutils.CommandError("Rapid hinting makes no sense "
- "with target {}!".format(name))
+ no_rapid_targets = [
+ Target.tab_fg, # opens new tab
+ Target.fill, # exits hint mode
+ Target.right_click, # opens multiple context menus
+ Target.delete, # deleting elements shifts them
+ ]
+ if rapid and target in no_rapid_targets:
+ name = target.name.replace('_', '-')
+ raise cmdutils.CommandError(
+ f"Rapid hinting makes no sense with target {name}!")
self._check_args(target, *args)
@@ -873,12 +871,11 @@ class HintManager(QObject):
label.update_text(matched, rest)
# Show label again if it was hidden before
label.show()
- else:
+ elif (not self._context.rapid or
+ config.val.hints.hide_unmatched_rapid_hints):
# element doesn't match anymore -> hide it, unless in rapid
# mode and hide_unmatched_rapid_hints is false (see #1799)
- if (not self._context.rapid or
- config.val.hints.hide_unmatched_rapid_hints):
- label.hide()
+ label.hide()
except webelem.Error:
pass
self._handle_auto_follow(keystr=keystr)
@@ -1157,7 +1154,6 @@ class WordHinter:
from the words arg as fallback.
Args:
- words: Words to use as fallback when no link text can be used.
elems: The elements to get hint strings for.
Return:
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
index 559992327..d2046345f 100644
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -405,6 +405,7 @@ class WebHistory(sql.SqlTable):
Args:
url: A url (as QUrl) to add to the history.
+ title: The tab title to add.
redirect: Whether the entry was redirected to another URL
(hidden in completion)
atime: Override the atime used to add the entry
diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py
index 82bf57136..6217c8d00 100644
--- a/qutebrowser/browser/navigate.py
+++ b/qutebrowser/browser/navigate.py
@@ -92,9 +92,6 @@ def incdec(url, count, inc_or_dec):
url: The current url.
count: How much to increment or decrement by.
inc_or_dec: Either 'increment' or 'decrement'.
- tab: Whether to open the link in a new tab.
- background: Open the link in a new background tab.
- window: Open the link in a new window.
"""
urlutils.ensure_valid(url)
segments: Optional[Set[str]] = (
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index e178fb0e5..8adb7ea20 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -110,6 +110,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
"""Create a file object using the internal filename."""
assert self._filename is not None
try:
+ # pylint: disable=consider-using-with
fileobj = open(self._filename, 'wb')
except OSError as e:
self._die(e.strerror)
@@ -202,6 +203,17 @@ class DownloadItem(downloads.AbstractDownloadItem):
# Note: self._reply is deleted when the download finishes
return self._url
+ def origin(self) -> QUrl:
+ if self._reply is None:
+ return QUrl()
+ origin = self._reply.request().originatingObject()
+ try:
+ return origin.url()
+ except AttributeError:
+ # Raised either if origin is None or some object that doesn't
+ # have its own url.
+ return QUrl()
+
def _ensure_can_set_filename(self, filename):
if self.fileobj is not None: # pragma: no cover
raise ValueError("fileobj was already set! filename: {}, "
@@ -491,6 +503,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
Args:
request: The QNetworkRequest to download.
target: Where to save the download as downloads.DownloadTarget.
+ suggested_fn: The filename to use for the file.
**kwargs: Passed to _fetch_request.
Return:
@@ -535,6 +548,9 @@ class DownloadManager(downloads.AbstractDownloadManager):
target: Where to save the download as downloads.DownloadTarget.
auto_remove: Whether to remove the download even if
downloads.remove_finished is set to -1.
+ suggested_filename: The filename to use for the file.
+ prompt_download_directory: Whether to prompt for a location to
+ download the file to.
Return:
The created DownloadItem.
@@ -549,6 +565,9 @@ class DownloadManager(downloads.AbstractDownloadManager):
download = DownloadItem(reply, manager=self)
self._init_item(download, auto_remove, suggested_filename)
+ if download.cancel_for_origin():
+ return download
+
if target is not None:
download.set_target(target)
return download
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 8d3ebe730..f195bbf28 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -51,7 +51,9 @@ def custom_headers(url):
conf_headers = config.instance.get('content.headers.custom', url=url)
for header, value in conf_headers.items():
- headers[header.encode('ascii')] = value.encode('ascii')
+ encoded_header = header.encode('ascii')
+ encoded_value = b"" if value is None else value.encode('ascii')
+ headers[encoded_header] = encoded_value
accept_language = config.instance.get('content.headers.accept_language',
url=url)
@@ -418,12 +420,11 @@ def choose_file(qb_mode: FileSelectionMode) -> List[str]:
}[qb_mode]
use_tmp_file = any('{}' in arg for arg in command[1:])
if use_tmp_file:
- handle = tempfile.NamedTemporaryFile(
+ with tempfile.NamedTemporaryFile(
prefix='qutebrowser-fileselect-',
delete=False,
- )
- handle.close()
- tmpfilename = handle.name
+ ) as handle:
+ tmpfilename = handle.name
with utils.cleanup_file(tmpfilename):
command = (
command[:1] +
@@ -509,6 +510,7 @@ def _validated_selected_files(
)
continue
else:
+ # pylint: disable=else-if-used
if not os.path.isfile(selected_file):
message.warning(
f"Expected file but got folder, ignoring '{selected_file}'"
diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py
index 0b002e345..88ac4a65d 100644
--- a/qutebrowser/browser/signalfilter.py
+++ b/qutebrowser/browser/signalfilter.py
@@ -88,6 +88,7 @@ class SignalFilter(QObject):
debug.dbg_signal(signal, args), tabidx))
signal.emit(*args)
else:
+ # pylint: disable=else-if-used
if log_signal:
log.signals.debug("ignoring: {} (tab {})".format(
debug.dbg_signal(signal, args), tabidx))
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index 9ec29ce07..05b0eadb3 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -132,6 +132,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
"""Dispatch an event to the element.
Args:
+ event: The name of the event.
bubbles: Whether this event should bubble.
cancelable: Whether this event can be cancelled.
composed: Whether the event will trigger listeners outside of a
@@ -160,9 +161,6 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
def is_content_editable(self) -> bool:
"""Check if an element has a contenteditable attribute.
- Args:
- elem: The QWebElement to check.
-
Return:
True if the element has a contenteditable attribute,
False otherwise.
@@ -233,6 +231,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
'span': ['cm-'], # Jupyter Notebook
}
relevant_classes = classes[self.tag_name()]
+ # pylint: disable=consider-using-any-or-all
for klass in self.classes():
if any(klass.strip().startswith(e) for e in relevant_classes):
return True
diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py
index 6b26157e6..69c702aec 100644
--- a/qutebrowser/browser/webengine/notification.py
+++ b/qutebrowser/browser/webengine/notification.py
@@ -65,7 +65,8 @@ if TYPE_CHECKING:
from qutebrowser.config import config
from qutebrowser.misc import objects
-from qutebrowser.utils import qtutils, log, utils, debug, message, version
+from qutebrowser.utils import qtutils, log, utils, debug, message, version, objreg
+from qutebrowser.qt import sip
bridge: Optional['NotificationBridgePresenter'] = None
@@ -367,6 +368,17 @@ class NotificationBridgePresenter(QObject):
# https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html
log.misc.debug(f"Ignoring click request for notification {notification_id} "
"due to PyQt bug")
+ return
+ self._focus_first_matching_tab(notification)
+
+ def _focus_first_matching_tab(self, notification: "QWebEngineNotification") -> None:
+ for win_id in objreg.window_registry:
+ tabbedbrowser = objreg.get("tabbed-browser", window=win_id, scope="window")
+ for idx, tab in enumerate(tabbedbrowser.widgets()):
+ if tab.url().matches(notification.origin(), QUrl.RemovePath):
+ tabbedbrowser.widget.setCurrentIndex(idx)
+ return
+ log.misc.debug(f"No matching tab found for {notification.origin()}")
def _drop_adapter(self) -> None:
"""Drop the currently active adapter (if any).
@@ -477,7 +489,9 @@ class SystrayNotificationAdapter(AbstractNotificationAdapter):
@pyqtSlot(int)
def on_web_closed(self, notification_id: int) -> None:
assert notification_id == self.NOTIFICATION_ID, notification_id
- self._systray.hide()
+ if not sip.isdeleted(self._systray):
+ # This can get called during shutdown
+ self._systray.hide()
class MessagesNotificationAdapter(AbstractNotificationAdapter):
@@ -712,10 +726,14 @@ 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:
- super().__init__(bridge)
+ super().__init__(parent)
assert _notifications_supported()
if utils.is_windows:
diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py
index fc7ed8ca2..a6a2a1b93 100644
--- a/qutebrowser/browser/webengine/webenginedownloads.py
+++ b/qutebrowser/browser/webengine/webenginedownloads.py
@@ -119,6 +119,10 @@ class DownloadItem(downloads.AbstractDownloadItem):
def url(self) -> QUrl:
return self._qt_item.url()
+ def origin(self) -> QUrl:
+ page = self._qt_item.page()
+ return page.url() if page else QUrl()
+
def _set_fileobj(self, fileobj, *, autoclose=True):
raise downloads.UnsupportedOperationError
@@ -251,7 +255,6 @@ class DownloadManager(downloads.AbstractDownloadManager):
qt_filename = os.path.basename(qt_item.path()) # FIXME use 5.14 API
mime_type = qt_item.mimeType()
url = qt_item.url()
- origin = qt_item.page().url() if qt_item.page() else QUrl()
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-90355
if version.qtwebengine_versions().webengine >= utils.VersionNumber(5, 15, 3):
@@ -293,9 +296,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
download.set_target(target)
return
- if url.scheme() == "file" and origin.isValid() and origin.scheme() == "file":
- utils.open_file(url.toLocalFile())
- qt_item.cancel()
+ if download.cancel_for_origin():
return
# Ask the user for a filename - needs to be blocking!
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index d20a92014..5430cec77 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -431,11 +431,7 @@ def _init_site_specific_quirks():
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/99 "
"Safari/537.36")
- edge_ua = ("Mozilla/5.0 ({os_info}) "
- "AppleWebKit/{webkit_version} (KHTML, like Gecko) "
- "{upstream_browser_key}/{upstream_browser_version} "
- "Safari/{webkit_version} "
- "Edg/{upstream_browser_version}")
+ firefox_ua = "Mozilla/5.0 ({os_info}; rv:90.0) Gecko/20100101 Firefox/90.0"
user_agents = [
# Needed to avoid a ""WhatsApp works with Google Chrome 36+" error
@@ -447,7 +443,7 @@ def _init_site_specific_quirks():
# Needed to avoid a "you're using a browser [...] that doesn't allow us
# to keep your account secure" error.
# https://github.com/qutebrowser/qutebrowser/issues/5182
- ("ua-google", 'https://accounts.google.com/*', edge_ua),
+ ("ua-google", 'https://accounts.google.com/*', firefox_ua),
# Needed because Slack adds an error which prevents using it relatively
# aggressively, despite things actually working fine.
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index ace23d14a..7d355d10e 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -26,7 +26,8 @@ import re
import html as html_utils
from typing import cast, Union, Optional
-from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl, QObject
+from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl,
+ QObject)
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript, QWebEngineHistory
@@ -200,6 +201,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 +255,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')
@@ -797,13 +803,38 @@ class WebEngineAudio(browsertab.AbstractAudio):
super().__init__(tab, parent)
self._overridden = False
+ # Implements the intended two-second delay specified at
+ # https://doc.qt.io/qt-5/qwebenginepage.html#recentlyAudibleChanged
+ delay_ms = 2000
+ self._silence_timer = QTimer(self)
+ self._silence_timer.setSingleShot(True)
+ self._silence_timer.setInterval(delay_ms)
+
def _connect_signals(self):
page = self._widget.page()
page.audioMutedChanged.connect(self.muted_changed)
- page.recentlyAudibleChanged.connect(self.recently_audible_changed)
+ page.recentlyAudibleChanged.connect(self._delayed_recently_audible_changed)
self._tab.url_changed.connect(self._on_url_changed)
config.instance.changed.connect(self._on_config_changed)
+ # WORKAROUND for recentlyAudibleChanged being emitted without delay from the moment
+ # that audio is dropped.
+ def _delayed_recently_audible_changed(self, recently_audible):
+ timer = self._silence_timer
+ # Stop any active timer and immediately display [A] if tab is audible,
+ # otherwise start a timer to update audio field
+ if recently_audible:
+ if timer.isActive():
+ timer.stop()
+ self.recently_audible_changed.emit(recently_audible)
+ else:
+ # Ignore all subsequent calls while the tab is muted with an active timer
+ if timer.isActive():
+ return
+ timer.timeout.connect(
+ functools.partial(self.recently_audible_changed.emit, recently_audible))
+ timer.start()
+
def set_muted(self, muted: bool, override: bool = False) -> None:
was_muted = self.is_muted()
self._overridden = override
@@ -1499,7 +1530,6 @@ class WebEngineTab(browsertab.AbstractTab):
displayed.
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66643
- WORKAROUND for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=882805
"""
match = re.search(r'"errorCode":"([^"]*)"', html)
if match is None:
@@ -1508,8 +1538,7 @@ class WebEngineTab(browsertab.AbstractTab):
error = match.group(1)
log.webview.error("Load error: {}".format(error))
- missing_jst = 'jstProcess(' in html and 'jstProcess=' not in html
- if js_enabled and not missing_jst:
+ if js_enabled:
return
self._show_error_page(self.url(), error=error)
diff --git a/qutebrowser/browser/webkit/http.py b/qutebrowser/browser/webkit/http.py
index eacb95679..e43d8fdef 100644
--- a/qutebrowser/browser/webkit/http.py
+++ b/qutebrowser/browser/webkit/http.py
@@ -101,8 +101,15 @@ class ContentDisposition:
if defects != [cls._IGNORED_DEFECT]: # type: ignore[comparison-overlap]
raise ContentDispositionError(defects)
- assert isinstance(parsed, email.headerregistry.ContentDispositionHeader), parsed
- return cls(disposition=parsed.content_disposition, params=parsed.params)
+ # https://github.com/python/mypy/issues/12314
+ assert isinstance(
+ parsed, # type: ignore[unreachable]
+ email.headerregistry.ContentDispositionHeader,
+ ), parsed
+ return cls( # type: ignore[unreachable]
+ disposition=parsed.content_disposition,
+ params=parsed.params,
+ )
def filename(self):
"""The filename from the Content-Disposition header or None.
diff --git a/qutebrowser/browser/webkit/webkithistory.py b/qutebrowser/browser/webkit/webkithistory.py
index 4149bda88..6ffe65193 100644
--- a/qutebrowser/browser/webkit/webkithistory.py
+++ b/qutebrowser/browser/webkit/webkithistory.py
@@ -44,7 +44,7 @@ class WebHistoryInterface(QWebHistoryInterface):
"""Required for a QWebHistoryInterface impl, obsoleted by add_url."""
@debugcachestats.register(name='history')
- @functools.lru_cache(maxsize=32768)
+ @functools.lru_cache(maxsize=32768) # noqa: B019
def historyContains(self, url_string):
"""Called by WebKit to determine if a URL is contained in the history.
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/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py
index 0242bed0c..289e29920 100644
--- a/qutebrowser/browser/webkit/webview.py
+++ b/qutebrowser/browser/webkit/webview.py
@@ -152,9 +152,6 @@ class WebView(QWebView):
Args:
e: The QPaintEvent.
-
- Return:
- The superclass event return value.
"""
frame = self.page().mainFrame()
new_pos = (frame.scrollBarValue(Qt.Horizontal),
@@ -186,9 +183,6 @@ class WebView(QWebView):
Args:
e: The QShowEvent.
-
- Return:
- The superclass event return value.
"""
super().showEvent(e)
self.page().setVisibilityState(QWebPage.VisibilityStateVisible)
@@ -198,9 +192,6 @@ class WebView(QWebView):
Args:
e: The QHideEvent.
-
- Return:
- The superclass event return value.
"""
super().hideEvent(e)
self.page().setVisibilityState(QWebPage.VisibilityStateHidden)
diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py
index f8f083b72..2a11589f9 100644
--- a/qutebrowser/commands/argparser.py
+++ b/qutebrowser/commands/argparser.py
@@ -101,7 +101,7 @@ def type_conv(param, typ, value, *, str_choices=None):
Args:
param: The argparse.Parameter we're checking
- types: The allowed type
+ typ: The allowed type
value: The value to convert
str_choices: The allowed choices if the type ends up being a string
diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py
index 653f551ff..eee5b7cde 100644
--- a/qutebrowser/commands/command.py
+++ b/qutebrowser/commands/command.py
@@ -272,11 +272,10 @@ class Command:
if is_bool:
kwargs['action'] = 'store_true'
+ elif arg_info.metavar is not None:
+ kwargs['metavar'] = arg_info.metavar
else:
- if arg_info.metavar is not None:
- kwargs['metavar'] = arg_info.metavar
- else:
- kwargs['metavar'] = argparser.arg_name(param.name)
+ kwargs['metavar'] = argparser.arg_name(param.name)
if param.kind == inspect.Parameter.VAR_POSITIONAL:
kwargs['nargs'] = '*' if self._star_args_optional else '+'
@@ -320,9 +319,8 @@ class Command:
self.opt_args[param.name] = long_flag, short_flag
if not is_bool:
self.flags_with_args += [short_flag, long_flag]
- else:
- if not arg_info.hide:
- self.pos_args.append((param.name, name))
+ elif not arg_info.hide:
+ self.pos_args.append((param.name, name))
return args
def _get_type(self, param):
diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py
index 70c639207..8282aa7c7 100644
--- a/qutebrowser/commands/userscripts.py
+++ b/qutebrowser/commands/userscripts.py
@@ -342,9 +342,8 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
self._kwargs = kwargs
try:
- handle = tempfile.NamedTemporaryFile(delete=False)
- handle.close()
- self._filepath = handle.name
+ with tempfile.NamedTemporaryFile(delete=False) as handle:
+ self._filepath = handle.name
except OSError as e:
message.error("Error while creating tempfile: {}".format(e))
return
diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py
index 778333854..8b0de9d8a 100644
--- a/qutebrowser/completion/completer.py
+++ b/qutebrowser/completion/completer.py
@@ -39,7 +39,7 @@ class CompletionInfo:
"""Context passed into all completion functions."""
config: config.Config
- keyconf: config.KeyConfig
+ keyconf: config.KeyConfig # pylint: disable=used-before-assignment
win_id: int
cur_tab: 'browsertab.AbstractTab'
diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py
index 81f4bba8e..236b25533 100644
--- a/qutebrowser/completion/models/completionmodel.py
+++ b/qutebrowser/completion/models/completionmodel.py
@@ -70,6 +70,7 @@ class CompletionModel(QAbstractItemModel):
Args:
index: The QModelIndex to get item flags for.
+ role: The Qt ItemRole to get the data for.
Return: The item data, or None on an invalid index.
"""
diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py
index 736d09644..7c8473b3f 100644
--- a/qutebrowser/completion/models/configmodel.py
+++ b/qutebrowser/completion/models/configmodel.py
@@ -78,7 +78,7 @@ def value(optname, *values, info):
Args:
optname: The name of the config option this model shows.
- values: The values already provided on the command line.
+ *values: The values already provided on the command line.
info: A CompletionInfo instance.
"""
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
@@ -137,6 +137,7 @@ def bind(key, *, info):
Args:
key: the key being bound.
+ info: A CompletionInfo instance.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
data = _bind_current_default(key, info)
diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py
index 7642bf904..77072c720 100644
--- a/qutebrowser/completion/models/miscmodels.py
+++ b/qutebrowser/completion/models/miscmodels.py
@@ -103,17 +103,23 @@ def session(*, info=None):
return model
-def _tabs(*, win_id_filter=lambda _win_id: True, add_win_id=True):
+def _tabs(*, win_id_filter=lambda _win_id: True, add_win_id=True, cur_win_id=None):
"""Helper to get the completion model for tabs/other_tabs.
Args:
win_id_filter: A filter function for window IDs to include.
Should return True for all included windows.
add_win_id: Whether to add the window ID to the completion items.
+ cur_win_id: Window ID to be passed from info.win_id
"""
def delete_tab(data):
"""Close the selected tab."""
- win_id, tab_index = data[0].split('/')
+ if cur_win_id is None:
+ win_id, tab_index = data[0].split('/')
+ else:
+ win_id = cur_win_id
+ tab_index = data[0]
+
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=int(win_id))
tabbed_browser.on_tab_close_requested(int(tab_index) - 1)
@@ -177,13 +183,15 @@ def other_tabs(*, info):
Used for the tab-take command.
"""
- return _tabs(win_id_filter=lambda win_id: win_id != info.win_id)
+ return _tabs(
+ win_id_filter=lambda win_id: win_id != info.win_id,
+ cur_win_id=info.win_id)
def tab_focus(*, info):
"""A model to complete on open tabs in the current window."""
model = _tabs(win_id_filter=lambda win_id: win_id == info.win_id,
- add_win_id=False)
+ add_win_id=False, cur_win_id=info.win_id)
special = [
("last", "Focus the last-focused tab"),
@@ -230,9 +238,7 @@ def _qdatetime_to_completion_format(qdate):
if not qdate.isValid():
ts = 0
else:
- ts = qdate.toMSecsSinceEpoch()
- if ts < 0:
- ts = 0
+ ts = max(qdate.toMSecsSinceEpoch(), 0)
pydate = datetime.datetime.fromtimestamp(ts / 1000)
return pydate.strftime(config.val.completion.timestamp_format)
diff --git a/qutebrowser/components/braveadblock.py b/qutebrowser/components/braveadblock.py
index 21319cb1b..b1e5b8a29 100644
--- a/qutebrowser/components/braveadblock.py
+++ b/qutebrowser/components/braveadblock.py
@@ -266,14 +266,13 @@ class BraveAdBlocker:
except DeserializationError:
message.error("Reading adblock filter data failed (corrupted data?). "
"Please run :adblock-update.")
- else:
- if (
- config.val.content.blocking.adblock.lists
- and not self._has_basedir
- and config.val.content.blocking.enabled
- and self.enabled
- ):
- message.info("Run :adblock-update to get adblock lists.")
+ elif (
+ config.val.content.blocking.adblock.lists
+ and not self._has_basedir
+ and config.val.content.blocking.enabled
+ and self.enabled
+ ):
+ message.info("Run :adblock-update to get adblock lists.")
def adblock_update(self) -> blockutils.BlocklistDownloads:
"""Update the adblock block lists."""
diff --git a/qutebrowser/components/hostblock.py b/qutebrowser/components/hostblock.py
index 2d6086245..191719f10 100644
--- a/qutebrowser/components/hostblock.py
+++ b/qutebrowser/components/hostblock.py
@@ -64,9 +64,10 @@ def get_fileobj(byte_io: IO[bytes]) -> IO[bytes]:
byte_io.seek(0) # rewind downloaded file
if zipfile.is_zipfile(byte_io):
byte_io.seek(0) # rewind what zipfile.is_zipfile did
- zf = zipfile.ZipFile(byte_io)
- filename = _guess_zip_filename(zf)
- byte_io = zf.open(filename, mode="r")
+ with zipfile.ZipFile(byte_io) as zf:
+ filename = _guess_zip_filename(zf)
+ # pylint: disable=consider-using-with
+ byte_io = zf.open(filename, mode="r")
else:
byte_io.seek(0) # rewind what zipfile.is_zipfile did
return byte_io
@@ -132,10 +133,16 @@ class HostBlocker:
host = request_url.host()
- return any(
- hostname in self._blocked_hosts or hostname in self._config_blocked_hosts
- for hostname in urlutils.widened_hostnames(host)
- )
+ if config.get("content.blocking.hosts.block_subdomains"):
+ return any(
+ hostname in self._blocked_hosts
+ or hostname in self._config_blocked_hosts
+ for hostname in urlutils.widened_hostnames(host)
+ )
+ else:
+ return (
+ host in self._blocked_hosts or host in self._config_blocked_hosts
+ )
def filter_request(self, info: interceptor.Request) -> None:
"""Block the given request if necessary."""
diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py
index 120806bfe..fe908b7d2 100644
--- a/qutebrowser/components/misccommands.py
+++ b/qutebrowser/components/misccommands.py
@@ -17,6 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
+# To allow count being documented
+# pylint: disable=differing-param-doc
+
"""Various commands."""
import os
@@ -183,7 +186,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/components/utils/blockutils.py b/qutebrowser/components/utils/blockutils.py
index bd27baece..98681a488 100644
--- a/qutebrowser/components/utils/blockutils.py
+++ b/qutebrowser/components/utils/blockutils.py
@@ -125,7 +125,7 @@ class BlocklistDownloads(QObject):
filename: path to a local file to import.
"""
try:
- fileobj = open(filename, "rb")
+ fileobj = open(filename, "rb") # pylint: disable=consider-using-with
except OSError as e:
message.error(
"blockutils: Error while reading {}: {}".format(filename, e.strerror)
diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py
index 437a54a33..834709ae6 100644
--- a/qutebrowser/config/config.py
+++ b/qutebrowser/config/config.py
@@ -387,6 +387,8 @@ class Config(QObject):
"""Get the given setting converted for Python code.
Args:
+ name: The name of the setting to get.
+ url: The URL to get the value for.
fallback: Use the global value if there's no URL-specific one.
"""
opt = self.get_opt(name)
diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py
index 143b02fca..407c74214 100644
--- a/qutebrowser/config/configcommands.py
+++ b/qutebrowser/config/configcommands.py
@@ -101,7 +101,7 @@ class ConfigCommands:
Args:
option: The name of the option.
value: The value to set.
- pattern: The URL pattern to use.
+ pattern: The link:configuring{outfilesuffix}#patterns[URL pattern] to use.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
@@ -151,9 +151,12 @@ class ConfigCommands:
default: If given, restore a default binding.
"""
if key is None:
+ url = QUrl('qute://bindings')
+ if mode != "normal":
+ url.setFragment(mode)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
- tabbed_browser.load_url(QUrl('qute://bindings'), newtab=True)
+ tabbed_browser.load_url(url, newtab=True)
return
seq = self._parse_key(key)
@@ -204,8 +207,8 @@ class ConfigCommands:
Args:
option: The name of the option.
- values: The values to cycle through.
- pattern: The URL pattern to use.
+ *values: The values to cycle through.
+ pattern: The link:configuring{outfilesuffix}#patterns[URL pattern] to use.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
@@ -259,7 +262,7 @@ class ConfigCommands:
Args:
option: The name of the option.
- pattern: The URL pattern to use.
+ pattern: The link:configuring{outfilesuffix}#patterns[URL pattern] to use.
temp: Set value temporarily until qutebrowser is closed.
"""
parsed_pattern = self._parse_pattern(pattern)
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 76c4ce369..95338b36f 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -239,6 +239,9 @@ qt.force_platformtheme:
based on the desktop environment.
qt.process_model:
+ renamed: qt.chromium.process_model
+
+qt.chromium.process_model:
type:
name: String
valid_values:
@@ -268,6 +271,9 @@ qt.process_model:
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
qt.low_end_device_mode:
+ renamed: qt.chromium.low_end_device_mode
+
+qt.chromium.low_end_device_mode:
type:
name: String
valid_values:
@@ -284,6 +290,39 @@ qt.low_end_device_mode:
This improves the RAM usage of renderer processes, at the expense of
performance.
+qt.chromium.sandboxing:
+ type:
+ name: String
+ valid_values:
+ - enable-all: Enable all available sandboxing mechanisms.
+ - disable-seccomp-bpf: Disable the Seccomp BPF filter sandbox (Linux only).
+ - disable-all: Disable all sandboxing (**not recommended!**).
+ default: enable-all
+ backend: QtWebEngine
+ restart: true
+ no_autoconfig: true # due to it being dangerous
+ # yamllint disable rule:line-length
+ desc: >-
+ What sandboxing mechanisms in Chromium to use.
+
+ Chromium has various sandboxing layers, which should be enabled for normal
+ browser usage. Mainly for testing and development, it's possible to disable
+ individual sandboxing layers via this setting.
+
+ Open `chrome://sandbox` to see the current sandbox status.
+
+ Changing this setting is only recommended if you know what you're doing, as
+ it **disables one of Chromium's security layers**. To avoid sandboxing being
+ accidentally disabled persistently, this setting can only be set via
+ `config.py`, not via `:set`.
+
+ See the Chromium documentation for more details:
+
+ - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/linux/sandboxing.md[Linux]
+ - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox.md[Windows]
+ - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox_faq.md[FAQ (Windows-centric)]
+ # yamllint enable rule:line-length
+
qt.highdpi:
type: Bool
default: false
@@ -616,6 +655,7 @@ content.headers.custom:
valtype:
name: String
encoding: ascii
+ none_ok: true
none_ok: true
supports_pattern: true
desc: Custom headers for qutebrowser HTTP requests.
@@ -649,7 +689,9 @@ content.headers.referer:
When to send the Referer header.
The Referer header tells websites from which website you were coming from
- when visiting them.
+ when visiting them. Note that with QtWebEngine, websites can override this
+ preference by setting the `Referrer-Policy:` header, so that any websites
+ visited from them get the full referer.
No restart is needed with QtWebKit.
@@ -677,14 +719,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.
@@ -747,6 +789,15 @@ content.blocking.hosts.lists:
The file `~/.config/qutebrowser/blocked-hosts` is always read if it exists.
+content.blocking.hosts.block_subdomains:
+ default: true
+ type: Bool
+ desc: >-
+ Block subdomains of blocked hosts.
+
+ Note: If only a single subdomain is blocked but should be allowed, consider
+ using `content.blocking.whitelist` instead.
+
content.blocking.method:
default: auto
type:
@@ -1049,7 +1100,8 @@ content.proxy:
`http://...` URL.
Note that with QtWebEngine, it will take a couple of seconds until the
- change is applied, if this value is changed at runtime.
+ change is applied, if this value is changed at runtime. Authentication for
+ SOCKS proxies isn't supported due to Chromium limitations.
content.proxy_dns_requests:
default: true
@@ -1344,6 +1396,18 @@ downloads.position:
default: top
desc: Where to show the downloaded files.
+downloads.prevent_mixed_content:
+ type: Bool
+ default: true
+ supports_pattern: true
+ desc:
+ Automatically abort insecure (HTTP) downloads originating from secure
+ (HTTPS) pages.
+
+ For per-domain settings, the relevant URL is the URL initiating the
+ download, not the URL the download itself is coming from. It's not
+ recommended to set this setting to false globally.
+
downloads.remove_finished:
default: -1
type:
@@ -1371,12 +1435,16 @@ editor.command:
* `{line0}`: Same as `{line}`, but starting from index 0.
* `{column0}`: Same as `{column}`, but starting from index 0.
-
editor.encoding:
type: Encoding
default: utf-8
desc: Encoding to use for the editor.
+editor.remove_file:
+ default: true
+ type: Bool
+ desc: Delete the temporary file upon closing the editor.
+
## fileselect
fileselect.handler:
@@ -1605,11 +1673,16 @@ hints.selectors:
- '[role="link"]'
- '[role="option"]'
- '[role="button"]'
+ - '[role="tab"]'
+ - '[role="checkbox"]'
+ - '[role="menuitem"]'
+ - '[role="menuitemcheckbox"]'
+ - '[role="menuitemradio"]'
- '[ng-click]'
- '[ngClick]'
- '[data-ng-click]'
- '[x-ng-click]'
- - '[tabindex]'
+ - '[tabindex]:not([tabindex="-1"])'
links:
- 'a[href]'
- 'area[href]'
@@ -1753,10 +1826,10 @@ input.spatial_navigation:
Enable spatial navigation.
Spatial navigation consists in the ability to navigate between focusable
- elements in a Web page, such as hyperlinks and form controls, by using
- Left, Right, Up and Down arrow keys. For example, if the user presses the
- Right key, heuristics determine whether there is an element he might be
- trying to reach towards the right and which element he probably wants.
+ elements, such as hyperlinks and form controls, on a web page by using the
+ Left, Right, Up and Down arrow keys. For example, if a user presses the
+ Right key, heuristics determine whether there is an element they might be
+ trying to reach towards the right and which element they probably want.
input.media_keys:
default: true
@@ -1770,6 +1843,29 @@ 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.
+
+input.mode_override:
+ default: null
+ type:
+ name: String
+ none_ok: true
+ valid_values:
+ - normal
+ - insert
+ - passthrough
+ supports_pattern: true
+ desc: Mode to change to when focusing on a tab/URL changes.
+
## keyhint
keyhint.blacklist:
@@ -2131,6 +2227,7 @@ tabs.title.format:
- title_sep
- index
- aligned_index
+ - relative_index
- id
- scroll_pos
- host
@@ -2150,6 +2247,7 @@ tabs.title.format:
* `{index}`: Index of this tab.
* `{aligned_index}`: Index of this tab padded with spaces to have the same
width.
+ * `{relative_index}`: Index of this tab relative to the current tab.
* `{id}`: Internal tab ID of this tab.
* `{scroll_pos}`: Page scroll position.
* `{host}`: Host of the current web page.
@@ -2170,6 +2268,7 @@ tabs.title.format_pinned:
- title_sep
- index
- aligned_index
+ - relative_index
- id
- scroll_pos
- host
@@ -3630,6 +3729,7 @@ bindings.default:
<Ctrl-E>: edit-text
<Shift-Ins>: insert-text -- {primary}
<Escape>: mode-leave
+ <Shift-Escape>: fake-key <Escape>
hint:
<Return>: hint-follow
<Ctrl-R>: hint --rapid links tab-bg
@@ -3826,9 +3926,7 @@ bindings.commands:
that single keys can't be bound in this mode.
* passthrough: Similar to insert mode, but passes through all keypresses
- except `<Escape>` to leave the mode. It might be useful to bind
- `<Escape>` to some other key in this mode if you want to be able to send
- an Escape key to the website as well. Note that single keys can't be
+ except `<Shift+Escape>` to leave the mode. Note that single keys can't be
bound in this mode.
* command: Entered when pressing the `:` key in order to enter a command.
diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py
index 6f0d0b13c..d97771fad 100644
--- a/qutebrowser/config/configfiles.py
+++ b/qutebrowser/config/configfiles.py
@@ -384,8 +384,8 @@ class YamlMigrations(QObject):
changed = pyqtSignal()
- def __init__(self, settings: _SettingsType,
- parent: QObject = None) -> None:
+ # Note: settings is Any because it's not validated yet.
+ def __init__(self, settings: Any, parent: QObject = None) -> None:
super().__init__(parent)
self._settings = settings
diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py
index c38ef5b01..9e7f2620d 100644
--- a/qutebrowser/config/qtargs.py
+++ b/qutebrowser/config/qtargs.py
@@ -260,9 +260,8 @@ def _qtwebengine_args(
# Only actually available in Qt 5.12.5, but let's save another
# check, as passing the option won't hurt.
yield '--enable-in-process-stack-traces'
- else:
- if 'stack' not in namespace.debug_flags:
- yield '--disable-in-process-stack-traces'
+ elif 'stack' not in namespace.debug_flags:
+ yield '--disable-in-process-stack-traces'
lang_override = _get_lang_override(
webengine_version=versions.webengine,
@@ -322,12 +321,12 @@ def _qtwebengine_settings_args(versions: version.WebEngineVersions) -> Iterator[
'--force-webrtc-ip-handling-policy='
'disable_non_proxied_udp',
},
- 'qt.process_model': {
+ 'qt.chromium.process_model': {
'process-per-site-instance': None,
'process-per-site': '--process-per-site',
'single-process': '--single-process',
},
- 'qt.low_end_device_mode': {
+ 'qt.chromium.low_end_device_mode': {
'auto': None,
'always': '--enable-low-end-device-mode',
'never': '--disable-low-end-device-mode',
@@ -339,6 +338,11 @@ def _qtwebengine_settings_args(versions: version.WebEngineVersions) -> Iterator[
True: '--force-prefers-reduced-motion',
False: None,
},
+ 'qt.chromium.sandboxing': {
+ 'enable-all': None,
+ 'disable-seccomp-bpf': '--disable-seccomp-filter-sandbox',
+ 'disable-all': '--no-sandbox',
+ }
}
qt_514_ver = utils.VersionNumber(5, 14)
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
index 7556d2b6d..41aeec6a3 100644
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -238,6 +238,7 @@ def user_agent(url: QUrl = None) -> str:
def init(args: argparse.Namespace) -> None:
"""Initialize all QWeb(Engine)Settings."""
+ utils.unused(args)
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings
webenginesettings.init()
diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py
index 7ae45023b..793f131c8 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
@@ -84,7 +83,7 @@ def add_module_info(module: types.ModuleType) -> ModuleInfo:
# pylint: disable=protected-access
if not hasattr(module, '__qute_module_info'):
module.__qute_module_info = ModuleInfo() # type: ignore[attr-defined]
- return module.__qute_module_info # type: ignore[attr-defined]
+ return module.__qute_module_info
def load_components(*, skip_hooks: bool = False) -> None:
@@ -95,22 +94,8 @@ 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.
- path=components.__path__, # type: ignore[attr-defined]
+ path=components.__path__,
prefix=components.__name__ + '.',
onerror=_on_walk_error):
if ispkg:
@@ -123,23 +108,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 +158,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/bindings.html b/qutebrowser/html/bindings.html
index fe6913402..7bc441cbd 100644
--- a/qutebrowser/html/bindings.html
+++ b/qutebrowser/html/bindings.html
@@ -10,7 +10,7 @@ th { text-align:left; }
{% block content %}
<header><h1>{{ title }}</h1></header>
{% for mode, binding in bindings.items() %}
-<h2>{{ mode | capitalize }} mode</h2>
+<h2 id="{{ mode }}">{{ mode | capitalize }} mode</h2>
<table>
<tr>
<th>Key</th>
diff --git a/qutebrowser/html/settings.html b/qutebrowser/html/settings.html
index 44824eeac..b06917fd5 100644
--- a/qutebrowser/html/settings.html
+++ b/qutebrowser/html/settings.html
@@ -13,22 +13,145 @@ 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;
+ cursor: pointer;
+}
+
+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;
+}
+
+.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;
+}
+
+.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;
+}
+
+.option-description p {
+ margin: 0;
+}
+
+.long-description {
+ white-space: pre-line;
+}
+
+details summary > * {
+ display: inline;
+}
+
+details[open] .details {
+ display: none;
+}
+
+summary {
+ margin: .5ex 0;
+ width: fit-content;
+ color: #1887c5;
+ outline: none;
+ font-size: 105%;
+ cursor: pointer;
+}
+
+summary .short-description {
+ color: #635d5dcf;
+}
+
+summary::selection {
+ background-color: inherit;
+}
{% 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>
@@ -36,19 +159,51 @@ input { width: 98%; }
</tr>
{% for option in configdata.DATA.values()|sort(attribute='name') if not option.no_autoconfig %}
<tr>
+ {% set loopIndex = loop.index0 %}
<!-- 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>
+ {% set description = option.description.split('\n', 1) %}
+ <div class="option-description">
+ {% if description|length > 1 %}
+ <details>
+ <summary>
+ <p class="short-description">{{ description[0]|e }}</p>
+ <span class="details">Details</span>
+ </summary>
+ <p class="long-description">{{ description[1]|e }}</p>
+ </details>
+ {% else %}
+ <p>{{ description[0]|e }}</p>
+ {% endif %}
+ </div>
{% 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/html/warning-webkit.html b/qutebrowser/html/warning-webkit.html
index 975f98c1b..f5cf9bf01 100644
--- a/qutebrowser/html/warning-webkit.html
+++ b/qutebrowser/html/warning-webkit.html
@@ -41,8 +41,8 @@ hopefully help.</p>
notification support was added for Qt 5.13.0.</p>
<p><b>Resource usage</b>: qutebrowser v1.5.0 added the <span
-class="mono">qt.process_model</span> and <span
-class="mono">qt.low_end_device_mode</span> settings which can be used to
+class="mono">qt.chromium.process_model</span> and <span
+class="mono">qt.chromium.low_end_device_mode</span> settings which can be used to
decrease the resource usage of QtWebEngine (but come with other drawbacks).</p>
<p><b>Not trusting Google</b>: Various people have checked the connections made
@@ -78,7 +78,7 @@ security fixes took months to arrive there). You might be better off choosing an
method</a>.</p>
<p><b>White flashing between loads with a custom stylesheet</b>: This doesn't
-seem to happen with <span class="mono">qt.process_model = single-process</span>
+seem to happen with <span class="mono">qt.chromium.process_model = single-process</span>
set. However, note that that setting comes with decreased security and
stability, but QtWebKit doesn't have any process isolation at all.</p>
{% endblock %}
diff --git a/qutebrowser/javascript/greasemonkey_wrapper.js b/qutebrowser/javascript/greasemonkey_wrapper.js
index 7bfabc635..ced4e1063 100644
--- a/qutebrowser/javascript/greasemonkey_wrapper.js
+++ b/qutebrowser/javascript/greasemonkey_wrapper.js
@@ -77,6 +77,9 @@
if ("onerror" in details) {
oXhr.onerror = function () { details.onerror(oXhr); };
}
+ if ("overrideMimeType" in details) {
+ oXhr.overrideMimeType(details.overrideMimeType);
+ }
oXhr.open(details.method, details.url, true);
@@ -107,6 +110,22 @@
}
}
+ // Based on GreaseMonkey:
+ // https://github.com/greasemonkey/greasemonkey/blob/4.11/src/bg/api-provider-source.js#L232-L249
+ function GM_setClipboard(text) {
+ function onCopy(event) {
+ document.removeEventListener('copy', onCopy, true);
+
+ event.stopImmediatePropagation();
+ event.preventDefault();
+
+ event.clipboardData.setData('text/plain', text);
+ }
+
+ document.addEventListener('copy', onCopy, true);
+ document.execCommand('copy');
+ }
+
// Stub these two so that the gm4 polyfill script doesn't try to
// create broken versions as attributes of window.
function GM_getResourceText(caption, commandFunc, accessKey) {
@@ -123,6 +142,7 @@
const entries = {
'log': GM_log,
'addStyle': GM_addStyle,
+ 'setClipboard': GM_setClipboard,
'deleteValue': GM_deleteValue,
'getValue': GM_getValue,
'listValues': GM_listValues,
diff --git a/qutebrowser/javascript/quirks/globalthis.user.js b/qutebrowser/javascript/quirks/globalthis.user.js
index 103681fda..b87a956e5 100644
--- a/qutebrowser/javascript/quirks/globalthis.user.js
+++ b/qutebrowser/javascript/quirks/globalthis.user.js
@@ -1,6 +1,8 @@
// ==UserScript==
// @include https://www.reddit.com/*
// @include https://open.spotify.com/*
+// @include https://*.stackexchange.com/*
+// @include https://stackoverflow.com/*
// @include https://test.qutebrowser.org/*
// ==/UserScript==
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/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py
index 4d016bea6..f0d85b0ec 100644
--- a/qutebrowser/keyinput/eventfilter.py
+++ b/qutebrowser/keyinput/eventfilter.py
@@ -35,7 +35,7 @@ class EventFilter(QObject):
Attributes:
_activated: Whether the EventFilter is currently active.
- _handlers; A {QEvent.Type: callable} dict with the handlers for an
+ _handlers: A {QEvent.Type: callable} dict with the handlers for an
event.
"""
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
index 6bd8c99b8..f74c59aa7 100644
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -656,7 +656,6 @@ class KeySequence:
@classmethod
def parse(cls, keystr: str) -> 'KeySequence':
"""Parse a keystring like <Ctrl-x> or xyz and return a KeySequence."""
- # pylint: disable=protected-access
new = cls()
strings = list(_parse_keystring(keystr))
for sub in utils.chunk(strings, cls._MAX_LEN):
@@ -666,6 +665,5 @@ class KeySequence:
if keystr:
assert new, keystr
- # pylint: disable=protected-access
new._validate(keystr)
return new
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index d0723742a..7f62c2dc4 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -620,13 +620,15 @@ class MainWindow(QWidget):
True if closing is okay, False if a closeEvent should be ignored.
"""
tab_count = self.tabbed_browser.widget.count()
+ window_count = len(objreg.window_registry)
download_count = self._download_model.running_downloads()
quit_texts = []
# Ask if multiple-tabs are open
if 'multiple-tabs' in config.val.confirm_quit and tab_count > 1:
quit_texts.append("{} tabs are open.".format(tab_count))
- # Ask if multiple downloads running
- if 'downloads' in config.val.confirm_quit and download_count > 0:
+ # Ask if downloads running
+ if ('downloads' in config.val.confirm_quit and download_count > 0 and
+ window_count <= 1):
quit_texts.append("{} {} running.".format(
download_count,
"download is" if download_count == 1 else "downloads are"))
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index 1671ed46f..21976f383 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -536,9 +536,11 @@ class _BasePrompt(QWidget):
self.KEY_MODE.name)
labels = []
+ has_bindings = False
for cmd, text in self._allowed_commands():
bindings = all_bindings.get(cmd, [])
if bindings:
+ has_bindings = True
binding = None
preferred = ['<enter>', '<escape>']
for pref in preferred:
@@ -547,8 +549,11 @@ class _BasePrompt(QWidget):
if binding is None:
binding = bindings[0]
key_label = QLabel('<b>{}</b>'.format(html.escape(binding)))
- text_label = QLabel(text)
- labels.append((key_label, text_label))
+ else:
+ key_label = QLabel(f'<b>unbound</b> (<tt>{html.escape(cmd)}</tt>)')
+
+ text_label = QLabel(text)
+ labels.append((key_label, text_label))
for i, (key_label, text_label) in enumerate(labels):
self._key_grid.addWidget(key_label, i, 0)
@@ -559,6 +564,14 @@ class _BasePrompt(QWidget):
self._vbox.addLayout(self._key_grid)
+ if not has_bindings:
+ label = QLabel(
+ "<b>Note:</b> You seem to have unbound all keys for this prompt "
+ f"(<tt>{self.KEY_MODE.name}</tt> key mode)."
+ "<br/>Run <tt>qutebrowser :CMD</tt> with a command from above to "
+ "close this prompt, then fix this in your config.")
+ self._vbox.addWidget(label)
+
def _check_save_support(self, save):
if save:
raise UnsupportedOperationError("Saving answers is only possible "
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index e081284ee..a96f6d583 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -223,6 +223,10 @@ class TabbedBrowser(QWidget):
# https://bugreports.qt.io/browse/QTBUG-65223
self.cur_load_finished.connect(self._leave_modes_on_load)
+ # handle mode_override
+ self.current_tab_changed.connect(lambda tab: self._mode_override(tab.url()))
+ self.cur_url_changed.connect(self._mode_override)
+
# This init is never used, it is immediately thrown away in the next
# line.
self.undo_stack: UndoStackType = collections.deque()
@@ -406,15 +410,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
@@ -508,8 +513,7 @@ class TabbedBrowser(QWidget):
last_close_urlstr = urls[last_close].toString().rstrip('/')
first_tab_urlstr = first_tab_url.toString().rstrip('/')
last_close_url_used = first_tab_urlstr == last_close_urlstr
- use_current_tab = (only_one_tab_open and no_history and
- last_close_url_used)
+ use_current_tab = no_history and last_close_url_used
entries = self.undo_stack[-depth]
del self.undo_stack[-depth]
@@ -697,10 +701,9 @@ class TabbedBrowser(QWidget):
"""
if tab.data.keep_icon:
tab.data.keep_icon = False
- else:
- if (config.cache['tabs.tabs_are_windows'] and
- tab.data.should_show_icon()):
- self.widget.window().setWindowIcon(self.default_window_icon)
+ elif (config.cache['tabs.tabs_are_windows'] and
+ tab.data.should_show_icon()):
+ self.widget.window().setWindowIcon(self.default_window_icon)
@pyqtSlot()
def _on_load_status_changed(self, tab):
@@ -777,6 +780,23 @@ class TabbedBrowser(QWidget):
if not self.widget.page_title(idx):
self.widget.set_page_title(idx, url.toDisplayString())
+ def _mode_override(self, url: QUrl) -> None:
+ """Override mode if url matches pattern.
+
+ Args:
+ url: The QUrl to match for
+ """
+ if not url.isValid():
+ return
+ mode = config.instance.get('input.mode_override', url=url)
+ if mode:
+ log.modes.debug(f"Mode change to {mode} triggered for url {url}")
+ modeman.enter(
+ self._win_id,
+ usertypes.KeyMode[mode],
+ reason='mode_override',
+ )
+
@pyqtSlot(browsertab.AbstractTab)
def _on_icon_changed(self, tab):
"""Set the icon of a tab.
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index 7983127d5..511c2c309 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -136,18 +136,31 @@ class TabWidget(QTabWidget):
(fmt is None or ('{' + field + '}') not in fmt)):
return
+ def right_align(num):
+ return str(num).rjust(len(str(self.count())))
+
+ def left_align(num):
+ return str(num).ljust(len(str(self.count())))
+
+ bar = self.tabBar()
+ cur_idx = bar.currentIndex()
+ if idx == cur_idx:
+ rel_idx = left_align(idx + 1) + " "
+ else:
+ rel_idx = " " + right_align(abs(idx - cur_idx))
+
fields = self.get_tab_fields(idx)
fields['current_title'] = fields['current_title'].replace('&', '&&')
fields['index'] = idx + 1
- fields['aligned_index'] = str(idx + 1).rjust(len(str(self.count())))
+ fields['aligned_index'] = right_align(idx + 1)
+ fields['relative_index'] = rel_idx
title = '' if fmt is None else fmt.format(**fields)
- tabbar = self.tabBar()
# Only change the tab title if it changes, setting the tab title causes
# a size recalculation which is slow.
- if tabbar.tabText(idx) != title:
- tabbar.setTabText(idx, title)
+ if bar.tabText(idx) != title:
+ bar.setTabText(idx, title)
def get_tab_fields(self, idx):
"""Get the tab field data."""
@@ -305,6 +318,7 @@ class TabWidget(QTabWidget):
def _on_current_changed(self, index):
"""Emit the tab_index_changed signal if the current tab changed."""
self.tabBar().on_current_changed()
+ self.update_tab_titles()
self.tab_index_changed.emit(index, self.count())
@pyqtSlot()
@@ -396,6 +410,15 @@ class TabBar(QTabBar):
config.instance.changed.connect(self._on_config_changed)
self._set_icon_size()
QTimer.singleShot(0, self.maybe_hide)
+ self._minimum_tab_size_hint_helper = functools.lru_cache(maxsize=2**9)(
+ self._minimum_tab_size_hint_helper_uncached
+ )
+ debugcachestats.register(name=f'tab width cache (win_id={win_id})')(
+ self._minimum_tab_size_hint_helper
+ )
+ self._minimum_tab_height = functools.lru_cache(maxsize=1)(
+ self._minimum_tab_height_uncached
+ )
def __repr__(self):
return utils.get_repr(self, count=self.count())
@@ -486,7 +509,6 @@ class TabBar(QTabBar):
Args:
idx: The tab index to get the title for.
- handle_unset: Whether to return an empty string on KeyError.
"""
try:
return self.tab_data(idx, 'page-title')
@@ -562,11 +584,9 @@ class TabBar(QTabBar):
icon_width, ellipsis,
pinned)
- @debugcachestats.register(name='tab width cache')
- @functools.lru_cache(maxsize=2**9)
- def _minimum_tab_size_hint_helper(self, tab_text: str,
- icon_width: int,
- ellipsis: bool, pinned: bool) -> QSize:
+ def _minimum_tab_size_hint_helper_uncached(self, tab_text: str,
+ icon_width: int,
+ ellipsis: bool, pinned: bool) -> QSize:
"""Helper function to cache tab results.
Config values accessed in here should be added to _on_config_changed to
@@ -597,8 +617,7 @@ class TabBar(QTabBar):
width = max(min_width, width)
return QSize(width, height)
- @functools.lru_cache(maxsize=1)
- def _minimum_tab_height(self):
+ def _minimum_tab_height_uncached(self):
padding = config.cache['tabs.padding']
return self.fontMetrics().height() + padding.top + padding.bottom
diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py
index 45c52f54c..d94d3ec54 100644
--- a/qutebrowser/misc/crashsignal.py
+++ b/qutebrowser/misc/crashsignal.py
@@ -100,7 +100,7 @@ class CrashHandler(QObject):
# There's no log file, so we can use this to display crashes to
# the user on the next start.
self._init_crashlogfile()
- except OSError:
+ except (OSError, UnicodeDecodeError):
log.init.exception("Error while handling crash log file!")
self._init_crashlogfile()
@@ -150,6 +150,7 @@ class CrashHandler(QObject):
"""Start a new logfile and redirect faulthandler to it."""
logname = os.path.join(standarddir.data(), 'crash.log')
try:
+ # pylint: disable=consider-using-with
self._crash_log_file = open(logname, 'w', encoding='ascii')
except OSError:
log.init.exception("Error while opening crash log file!")
@@ -244,7 +245,7 @@ class CrashHandler(QObject):
if 'pdb-postmortem' in objects.debug_flags:
if tb is None:
- pdb.set_trace() # noqa: T100
+ pdb.set_trace() # noqa: T100 pylint: disable=forgotten-debug-statement
else:
pdb.post_mortem(tb)
diff --git a/qutebrowser/misc/debugcachestats.py b/qutebrowser/misc/debugcachestats.py
index 2004ad7ab..9090bd0ea 100644
--- a/qutebrowser/misc/debugcachestats.py
+++ b/qutebrowser/misc/debugcachestats.py
@@ -23,11 +23,15 @@ Because many modules depend on this command, this needs to have as few
dependencies as possible to avoid cyclic dependencies.
"""
-from typing import Any, Callable, List, Optional, Tuple, TypeVar
+import weakref
+import sys
+from typing import Any, Callable, Optional, TypeVar, Mapping
+from qutebrowser.utils import log
-# The second element of each tuple should be a lru_cache wrapped function
-_CACHE_FUNCTIONS: List[Tuple[str, Any]] = []
+
+# The callable should be a lru_cache wrapped function
+_CACHE_FUNCTIONS: Mapping[str, Any] = weakref.WeakValueDictionary()
_T = TypeVar('_T', bound=Callable[..., Any])
@@ -36,13 +40,21 @@ _T = TypeVar('_T', bound=Callable[..., Any])
def register(name: Optional[str] = None) -> Callable[[_T], _T]:
"""Register a lru_cache wrapped function for debug_cache_stats."""
def wrapper(fn: _T) -> _T:
- _CACHE_FUNCTIONS.append((fn.__name__ if name is None else name, fn))
- return fn
+ fn_name = fn.__name__ if name is None else name
+ if sys.version_info < (3, 9):
+ log.misc.vdebug( # type: ignore[attr-defined]
+ "debugcachestats not supported on python < 3.9, not adding '%s'",
+ fn_name,
+ )
+ return fn
+
+ else:
+ _CACHE_FUNCTIONS[fn_name] = fn
+ return fn
return wrapper
def debug_cache_stats() -> None:
"""Print LRU cache stats."""
- from qutebrowser.utils import log
- for name, fn in _CACHE_FUNCTIONS:
+ for name, fn in _CACHE_FUNCTIONS.items():
log.misc.info('{}: {}'.format(name, fn.cache_info()))
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index c4ff0bb85..034d7ff74 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -109,19 +109,25 @@ def init_faulthandler(fileobj=sys.__stderr__):
when sys.stderr got replaced, e.g. by "Python Tools for Visual Studio".
Args:
- fobj: An opened file object to write the traceback to.
+ fileobj: 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.
@@ -205,7 +211,6 @@ def _check_modules(modules):
for name, text in modules.items():
try:
- # pylint: disable=bad-continuation
with log.py_warning_filter(
category=DeprecationWarning,
message=r'invalid escape sequence'
@@ -220,7 +225,6 @@ def _check_modules(modules):
category=DeprecationWarning,
message=r'Creating a LegacyVersion has been deprecated',
):
- # pylint: enable=bad-continuation
importlib.import_module(name)
except ImportError as e:
_die(text, e)
diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py
index d561a7b96..3ef84284d 100644
--- a/qutebrowser/misc/editor.py
+++ b/qutebrowser/misc/editor.py
@@ -135,7 +135,7 @@ class ExternalEditor(QObject):
message.error("Failed to create initial file: {}".format(e))
return
- self._remove_file = True
+ self._remove_file = config.val.editor.remove_file
line, column = self._calc_line_and_column(text, caret_position)
self._start_editor(line=line, column=column)
diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py
index e5ccd1b8b..e14169f93 100644
--- a/qutebrowser/misc/guiprocess.py
+++ b/qutebrowser/misc/guiprocess.py
@@ -22,12 +22,13 @@
import dataclasses
import locale
import shlex
+import shutil
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
@@ -168,6 +169,7 @@ class GUIProcess(QObject):
self._output_messages = output_messages
self.outcome = ProcessOutcome(what=what)
self.cmd: Optional[str] = None
+ self.resolved_cmd: Optional[str] = None
self.args: Optional[Sequence[str]] = None
self.pid: Optional[int] = None
@@ -262,18 +264,19 @@ class GUIProcess(QObject):
QProcess.Crashed: f"{what.capitalize()} crashed",
QProcess.Timedout: f"{what.capitalize()} timed out",
QProcess.WriteError: f"Write error for {what}",
- QProcess.WriteError: f"Read error for {what}",
+ QProcess.ReadError: f"Read error for {what}",
}
error_string = self._proc.errorString()
msg = ': '.join([error_descriptions[error], error_string])
# We can't get some kind of error code from Qt...
# https://bugreports.qt.io/browse/QTBUG-44769
- # However, it looks like those strings aren't actually translated?
- 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)'
+ # but we pre-resolve the executable in Python, which also checks if it's
+ # runnable.
+ if self.resolved_cmd is None: # pragma: no branch
+ msg += f'\nHint: Make sure {self.cmd!r} exists and is executable'
+ if version.is_flatpak():
+ msg += ' inside the Flatpak container'
message.error(msg)
@@ -332,10 +335,23 @@ class GUIProcess(QObject):
self.outcome.running = True
def _pre_start(self, cmd: str, args: Sequence[str]) -> None:
- """Prepare starting of a QProcess."""
+ """Resolve the given command and prepare starting of a QProcess.
+
+ Doing the resolving in Python here instead of letting Qt do it serves
+ two purposes:
+
+ - Being able to show a nicer error message without having to parse the
+ string we get from Qt: https://bugreports.qt.io/browse/QTBUG-44769
+ - Not running the file from the current directory on Unix with
+ Qt < 5.15.? and 6.2.4, as a WORKAROUND for CVE-2022-25255:
+ https://invent.kde.org/qt/qt/qtbase/-/merge_requests/139
+ https://www.qt.io/blog/security-advisory-qprocess
+ https://lists.qt-project.org/pipermail/announce/2022-February/000333.html
+ """
if self.outcome.running:
raise ValueError("Trying to start a running QProcess!")
self.cmd = cmd
+ self.resolved_cmd = shutil.which(cmd)
self.args = args
log.procs.debug(f"Executing: {self}")
if self.verbose:
@@ -345,7 +361,10 @@ class GUIProcess(QObject):
"""Convenience wrapper around QProcess::start."""
log.procs.debug("Starting process.")
self._pre_start(cmd, args)
- self._proc.start(cmd, args)
+ self._proc.start(
+ self.resolved_cmd, # type: ignore[arg-type]
+ args,
+ )
self._post_start()
self._proc.closeWriteChannel()
@@ -354,7 +373,10 @@ class GUIProcess(QObject):
log.procs.debug("Starting detached.")
self._pre_start(cmd, args)
ok, self.pid = self._proc.startDetached(
- cmd, args, None) # type: ignore[call-arg]
+ self.resolved_cmd,
+ args,
+ None, # workingDirectory
+ ) # type: ignore[call-arg]
if not ok:
message.error("Error while spawning {}".format(self.what))
diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py
index 4fcef72e4..93d9af09d 100644
--- a/qutebrowser/misc/keyhintwidget.py
+++ b/qutebrowser/misc/keyhintwidget.py
@@ -87,6 +87,7 @@ class KeyHintView(QLabel):
"""Show hints for the given prefix (or hide if prefix is empty).
Args:
+ mode: The key mode to show the keyhints for.
prefix: The current partial keystring.
"""
match = re.fullmatch(r'(\d*)(.*)', prefix)
diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py
index 9d35692e9..fee87354f 100644
--- a/qutebrowser/misc/lineparser.py
+++ b/qutebrowser/misc/lineparser.py
@@ -99,6 +99,7 @@ class BaseLineParser(QObject):
self._opened = True
try:
if self._binary:
+ # pylint: disable=unspecified-encoding
with open(self._configfile, mode + 'b') as f:
yield f
else:
diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py
index b89288fc7..4354ed2ab 100644
--- a/qutebrowser/misc/miscwidgets.py
+++ b/qutebrowser/misc/miscwidgets.py
@@ -213,7 +213,7 @@ class _FoldArrow(QWidget):
"""Paint the arrow.
Args:
- _paint: The QPaintEvent (unused).
+ _event: The QPaintEvent (unused).
"""
opt = QStyleOption()
opt.initFrom(self)
diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py
index 9d5fbf601..4271c2639 100644
--- a/qutebrowser/misc/msgbox.py
+++ b/qutebrowser/misc/msgbox.py
@@ -42,6 +42,7 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
parent: The parent to set for the message box.
title: The title to set.
text: The text to set.
+ icon: The QIcon to show in the box.
buttons: The buttons to set (QMessageBox::StandardButtons)
on_finished: A slot to connect to the 'finished' signal.
plain_text: Whether to force plain text (True) or rich text (False).
diff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py
index a51891685..905429989 100644
--- a/qutebrowser/misc/quitter.py
+++ b/qutebrowser/misc/quitter.py
@@ -194,7 +194,7 @@ class Quitter(QObject):
# Open a new process and immediately shutdown the existing one
try:
args = self._get_restart_args(pages, session, override_args)
- subprocess.Popen(args)
+ subprocess.Popen(args) # pylint: disable=consider-using-with
except OSError:
log.destroy.exception("Failed to restart")
return False
diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py
index ee22ba14d..1b72734cb 100644
--- a/qutebrowser/misc/savemanager.py
+++ b/qutebrowser/misc/savemanager.py
@@ -157,6 +157,7 @@ class SaveManager(QObject):
"""Save a saveable by name.
Args:
+ name: The name of the saveable to save.
is_exit: Whether we're currently exiting qutebrowser.
explicit: Whether this save operation was triggered explicitly.
silent: Don't write information to log. Used to reduce log spam
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index 11af329e0..a28f3a848 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -206,12 +206,11 @@ class SessionManager(QObject):
if item.title():
data['title'] = item.title()
- else:
+ elif tab.history.current_idx() == idx:
# https://github.com/qutebrowser/qutebrowser/issues/879
- if tab.history.current_idx() == idx:
- data['title'] = tab.title()
- else:
- data['title'] = data['url']
+ data['title'] = tab.title()
+ else:
+ data['title'] = data['url']
if item.originalUrl() != item.url():
encoded = item.originalUrl().toEncoded()
diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py
index 4db91360e..c7d93e76d 100644
--- a/qutebrowser/misc/split.py
+++ b/qutebrowser/misc/split.py
@@ -128,6 +128,7 @@ def split(s, keep=False):
"""Split a string via ShellLexer.
Args:
+ s: The string to split.
keep: Whether to keep special chars in the split output.
"""
lexer = ShellLexer(s)
diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py
index 814eb2bb0..8f3282a2f 100644
--- a/qutebrowser/misc/sql.py
+++ b/qutebrowser/misc/sql.py
@@ -479,9 +479,6 @@ class SqlTable(QObject):
Args:
field: Field to use as the key.
value: Key value to delete.
-
- Return:
- The number of rows deleted.
"""
q = self.database.query(f"DELETE FROM {self._name} where {field} = :val")
q.run(val=value)
diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py
index 7ba45bdc3..14c02864e 100644
--- a/qutebrowser/misc/utilcmds.py
+++ b/qutebrowser/misc/utilcmds.py
@@ -23,6 +23,7 @@
import functools
import os
+import sys
import traceback
from typing import Optional
@@ -125,7 +126,9 @@ def debug_all_objects() -> None:
@cmdutils.register(debug=True)
def debug_cache_stats() -> None:
"""Print LRU cache stats."""
- debugcachestats.debug_cache_stats()
+ if sys.version_info < (3, 9):
+ raise cmdutils.CommandError('debugcachestats not supported on python < 3.9')
+ debugcachestats.debug_cache_stats() # type: ignore[unreachable]
@cmdutils.register(debug=True)
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/debug.py b/qutebrowser/utils/debug.py
index 7d069909a..0fa74f4e7 100644
--- a/qutebrowser/utils/debug.py
+++ b/qutebrowser/utils/debug.py
@@ -81,7 +81,7 @@ def log_signals(obj: QObject) -> QObject:
pass
if inspect.isclass(obj):
- old_init = obj.__init__ # type: ignore[misc]
+ old_init = obj.__init__
@functools.wraps(old_init)
def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
@@ -89,7 +89,7 @@ def log_signals(obj: QObject) -> QObject:
old_init(self, *args, **kwargs)
connect_log_slot(self)
- obj.__init__ = new_init # type: ignore[misc]
+ obj.__init__ = new_init
else:
connect_log_slot(obj)
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 9cd07e2e3..22035e074 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -44,6 +44,7 @@ except ImportError:
if TYPE_CHECKING:
from qutebrowser.config import config as configmodule
+ from typing import TextIO
_log_inited = False
_args = None
@@ -245,7 +246,9 @@ def disable_qt_msghandler() -> Iterator[None]:
@contextlib.contextmanager
def py_warning_filter(action: str = 'ignore', **kwargs: Any) -> Iterator[None]:
"""Contextmanager to temporarily disable certain Python warnings."""
- warnings.filterwarnings(action, **kwargs)
+ # FIXME Use Literal['default', 'error', 'ignore', 'always', 'module', 'once']
+ # once we use Python 3.8 or typing_extensions
+ warnings.filterwarnings(action, **kwargs) # type: ignore[arg-type]
yield
if _log_inited:
_init_py_warnings()
@@ -257,7 +260,7 @@ def _init_handlers(
force_color: bool,
json_logging: bool,
ram_capacity: int
-) -> Tuple[logging.StreamHandler, Optional['RAMHandler']]:
+) -> Tuple["logging.StreamHandler[TextIO]", Optional['RAMHandler']]:
"""Init log handlers.
Args:
@@ -381,8 +384,8 @@ def qt_message_handler(msg_type: QtCore.QtMsgType,
"""Qt message handler to redirect qWarning etc. to the logging system.
Args:
- QtMsgType msg_type: The level of the message.
- QMessageLogContext context: The source code location of the message.
+ msg_type: The level of the message.
+ context: The source code location of the message.
msg: The message text.
"""
# Mapping from Qt logging levels to the matching logging module levels.
@@ -727,7 +730,8 @@ class ColoredFormatter(logging.Formatter):
datefmt: str,
style: str, *,
use_colors: bool) -> None:
- super().__init__(fmt, datefmt, style)
+ # FIXME Use Literal["%", "{", "$"] once we use Python 3.8 or typing_extensions
+ super().__init__(fmt, datefmt, style) # type: ignore[arg-type]
self.use_colors = use_colors
def format(self, record: logging.LogRecord) -> str:
diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py
index 50a438637..c490aa4e8 100644
--- a/qutebrowser/utils/message.py
+++ b/qutebrowser/utils/message.py
@@ -18,7 +18,8 @@
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
# Because every method needs to have a log_stack argument
-# pylint: disable=unused-argument
+# and because we use *args a lot
+# pylint: disable=unused-argument,differing-param-doc
"""Message singleton so we don't have to define unneeded signals."""
@@ -125,7 +126,7 @@ def ask(*args: Any, **kwargs: Any) -> Any:
Return:
The answer the user gave or None if the prompt was cancelled.
"""
- question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
+ question = _build_question(*args, **kwargs)
global_bridge.ask(question, blocking=True)
answer = question.answer
question.deleteLater()
@@ -139,7 +140,7 @@ def ask_async(title: str,
"""Ask an async question in the statusbar.
Args:
- message: The message to display to the user.
+ title: The message to display to the user.
mode: A PromptMode.
handler: The function to get called with the answer as argument.
default: The default value to display.
@@ -174,7 +175,7 @@ def confirm_async(*, yes_action: _ActionType,
The question object.
"""
kwargs['mode'] = usertypes.PromptMode.yesno
- question = _build_question(**kwargs) # pylint: disable=missing-kwoa
+ question = _build_question(**kwargs)
question.answered_yes.connect(yes_action)
if no_action is not None:
question.answered_no.connect(no_action)
diff --git a/qutebrowser/utils/resources.py b/qutebrowser/utils/resources.py
index ff5ec9d9a..b14b0faf4 100644
--- a/qutebrowser/utils/resources.py
+++ b/qutebrowser/utils/resources.py
@@ -24,19 +24,25 @@ import sys
import contextlib
import posixpath
import pathlib
-from typing import Iterator, Iterable
+from typing import Iterator, Iterable, Union
# We cannot use the stdlib version on 3.7-3.8 because we need the files() API.
if sys.version_info >= (3, 9):
import importlib.resources as importlib_resources
+ from importlib.abc import Traversable
else: # pragma: no cover
import importlib_resources
+ from importlib_resources.abc import Traversable
import qutebrowser
_cache = {}
-def _path(filename: str) -> pathlib.Path:
+
+_ResourceType = Union[Traversable, pathlib.Path]
+
+
+def _path(filename: str) -> _ResourceType:
"""Get a pathlib.Path object for a resource."""
assert not posixpath.isabs(filename), filename
assert os.path.pardir not in filename.split(posixpath.sep), filename
@@ -64,7 +70,7 @@ def _keyerror_workaround() -> Iterator[None]:
def _glob(
- resource_path: pathlib.Path,
+ resource_path: _ResourceType,
subdir: str,
ext: str,
) -> Iterable[str]:
@@ -77,14 +83,11 @@ def _glob(
glob_path = resource_path / subdir
if isinstance(resource_path, pathlib.Path):
+ assert isinstance(glob_path, pathlib.Path)
for full_path in glob_path.glob(f'*{ext}'): # . is contained in ext
yield full_path.relative_to(resource_path).as_posix()
- 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
- # Python 3.8...
- assert glob_path.is_dir(), glob_path # type: ignore[unreachable]
+ else: # zipfile.Path or other importlib_resources.abc.Traversable
+ assert glob_path.is_dir(), glob_path
for subpath in glob_path.iterdir():
if subpath.name.endswith(ext):
yield posixpath.join(subdir, subpath.name)
diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py
index f1bd01a5f..c753e9de6 100644
--- a/qutebrowser/utils/standarddir.py
+++ b/qutebrowser/utils/standarddir.py
@@ -147,7 +147,8 @@ def _init_data(args: Optional[argparse.Namespace]) -> None:
# system_data
_locations.pop(_Location.system_data, None) # Remove old state
if utils.is_linux:
- path = '/usr/share/' + APPNAME
+ prefix = '/app' if version.is_flatpak() else '/usr'
+ path = f'{prefix}/share/{APPNAME}'
if os.path.exists(path):
_locations[_Location.system_data] = path
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index 002f10411..cfba2c1d8 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -95,6 +95,7 @@ def _parse_search_term(s: str) -> Tuple[Optional[str], Optional[str]]:
engine = None
term = s
else:
+ # pylint: disable=else-if-used
if config.val.url.open_base_url and s in config.val.url.searchengines:
engine = s
term = None
@@ -328,6 +329,7 @@ def invalid_url_error(url: QUrl, action: str) -> None:
"""Display an error message for a URL.
Args:
+ url: The URL to display a message for.
action: The action which was interrupted by the error.
"""
if url.isValid():
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index ee0f899cc..56c29899d 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -301,6 +301,7 @@ class Backend(enum.Enum):
"""The backend being used (usertypes.backend)."""
+ # pylint: disable=invalid-name
QtWebKit = enum.auto()
QtWebEngine = enum.auto()
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index a56769255..9c68932f3 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -382,7 +382,7 @@ def get_repr(obj: Any, constructor: bool = False, **attrs: Any) -> str:
obj: The object to get a repr for.
constructor: If True, show the Foo(one=1, two=2) form instead of
<Foo one=1 two=2>.
- attrs: The attributes to add.
+ **attrs: The attributes to add.
"""
cls = qualname(obj.__class__)
parts = []
@@ -391,11 +391,10 @@ def get_repr(obj: Any, constructor: bool = False, **attrs: Any) -> str:
parts.append('{}={!r}'.format(name, val))
if constructor:
return '{}({})'.format(cls, ', '.join(parts))
+ elif parts:
+ return '<{} {}>'.format(cls, ' '.join(parts))
else:
- if parts:
- return '<{} {}>'.format(cls, ' '.join(parts))
- else:
- return '<{}>'.format(cls)
+ return '<{}>'.format(cls)
def qualname(obj: Any) -> str:
@@ -669,11 +668,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..bf6b49fa6 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -511,10 +511,10 @@ def _get_pyqt_webengine_qt_version() -> Optional[str]:
give us an accurate answer.
"""
try:
- import importlib_metadata
+ import importlib.metadata as importlib_metadata # type: ignore[import]
except ImportError:
try:
- import importlib.metadata as importlib_metadata # type: ignore[no-redef]
+ import importlib_metadata # type: ignore[no-redef]
except ImportError:
log.misc.debug("Neither importlib.metadata nor backport available")
return None
@@ -723,7 +723,7 @@ class WebEngineVersions:
)
-def qtwebengine_versions(avoid_init: bool = False) -> WebEngineVersions:
+def qtwebengine_versions(*, avoid_init: bool = False) -> WebEngineVersions:
"""Get the QtWebEngine and Chromium version numbers.
If we have a parsed user agent, we use it here. If not, we avoid initializing
@@ -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 e6b75b5ef..25c1bd98f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,18 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-adblock==0.5.0
+adblock==0.5.2
colorama==0.4.4
dataclasses==0.6 ; python_version<"3.7"
-importlib-metadata==4.6.1 ; python_version<"3.8"
-importlib-resources==5.2.0 ; python_version<"3.9"
-Jinja2==3.0.1
-MarkupSafe==2.0.1
-Pygments==2.9.0
-PyYAML==5.4.1
-typing-extensions==3.10.0.0
-zipp==3.5.0
+importlib-metadata==4.11.3 ; python_version=="3.7.*"
+importlib-resources==5.6.0 ; python_version=="3.7.*" or python_version=="3.8.*"
+Jinja2==3.1.1 ; python_version>="3.7"
+MarkupSafe==2.1.1 ; python_version>="3.7"
+Pygments==2.11.2
+PyYAML==6.0
+typing_extensions==4.1.1 ; python_version<"3.8"
+zipp==3.7.0 ; python_version>="3.7"
+importlib-resources<5.6.0 ; python_version=="3.6.*"
+importlib-metadata<4.9 ; python_version=="3.6.*"
+zipp<3.7 ; python_version=="3.6.*"
+MarkupSafe<2.1.0 ; python_version=="3.6.*"
+Jinja2<3.1.0 ; python_version=="3.6.*"
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index a1c6646eb..797b15e52 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -26,6 +26,7 @@ import os.path
import sys
import time
import shutil
+import pathlib
import plistlib
import subprocess
import argparse
@@ -227,7 +228,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)
@@ -480,24 +481,27 @@ def build_sdist():
"""Build an sdist and list the contents."""
utils.print_title("Building sdist")
- _maybe_remove('dist')
+ dist_path = pathlib.Path('dist')
+ _maybe_remove(dist_path)
- subprocess.run([sys.executable, 'setup.py', 'sdist'], check=True)
- dist_files = os.listdir(os.path.abspath('dist'))
- assert len(dist_files) == 1
+ subprocess.run([sys.executable, '-m', 'build'], check=True)
- dist_file = os.path.join('dist', dist_files[0])
- subprocess.run(['gpg', '--detach-sign', '-a', dist_file], check=True)
+ dist_files = list(dist_path.glob('*.tar.gz'))
+ filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__)
+ assert dist_files == [dist_path / filename], dist_files
+ dist_file = dist_files[0]
+
+ subprocess.run(['gpg', '--detach-sign', '-a', str(dist_file)], check=True)
- tar = tarfile.open(dist_file)
by_ext = collections.defaultdict(list)
- for tarinfo in tar.getmembers():
- if not tarinfo.isfile():
- continue
- name = os.sep.join(tarinfo.name.split(os.sep)[1:])
- _base, ext = os.path.splitext(name)
- by_ext[ext].append(name)
+ with tarfile.open(dist_file) as tar:
+ for tarinfo in tar.getmembers():
+ if not tarinfo.isfile():
+ continue
+ name = os.sep.join(tarinfo.name.split(os.sep)[1:])
+ _base, ext = os.path.splitext(name)
+ by_ext[ext].append(name)
assert '.pyc' not in by_ext
@@ -507,11 +511,13 @@ def build_sdist():
utils.print_subtitle(ext)
print('\n'.join(files))
- filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__)
artifacts = [
- (os.path.join('dist', filename), 'application/gzip', 'Source release'),
- (os.path.join('dist', filename + '.asc'), 'application/pgp-signature',
- 'Source release - PGP signature'),
+ (str(dist_file), 'application/gzip', 'Source release'),
+ (
+ str(dist_file.with_suffix(dist_file.suffix + '.asc')),
+ 'application/pgp-signature',
+ 'Source release - PGP signature',
+ ),
]
return artifacts
@@ -550,6 +556,7 @@ def github_upload(artifacts, tag, gh_token):
Args:
artifacts: A list of (filename, mimetype, description) tuples
tag: The name of the release tag
+ gh_token: The GitHub token to use
"""
import github3
import github3.exceptions
@@ -589,9 +596,9 @@ def github_upload(artifacts, tag, gh_token):
assets = [asset for asset in release.assets()
if asset.name == basename]
if assets:
- asset = assets[0]
- print("Deleting stray asset {}".format(asset.name))
- asset.delete()
+ stray_asset = assets[0]
+ print("Deleting stray asset {}".format(stray_asset.name))
+ stray_asset.delete()
else:
break
@@ -599,15 +606,19 @@ def github_upload(artifacts, tag, gh_token):
def pypi_upload(artifacts):
"""Upload the given artifacts to PyPI using twine."""
utils.print_title("Uploading to PyPI...")
- filenames = [a[0] for a in artifacts]
- subprocess.run([sys.executable, '-m', 'twine', 'upload'] + filenames,
- check=True)
+ run_twine('upload', artifacts)
-def upgrade_sdist_dependencies():
- """Make sure we have the latest tools for an sdist release."""
- subprocess.run([sys.executable, '-m', 'pip', 'install', '-U', 'twine',
- 'pip', 'wheel', 'setuptools'], check=True)
+def twine_check(artifacts):
+ """Check packages using 'twine check'."""
+ utils.print_title("Running twine check...")
+ run_twine('check', artifacts, '--strict')
+
+
+def run_twine(command, artifacts, *args):
+ filenames = [a[0] for a in artifacts]
+ subprocess.run([sys.executable, '-m', 'twine', command] + list(args) + filenames,
+ check=True)
def main():
@@ -667,9 +678,9 @@ def main():
elif sys.platform == 'darwin':
artifacts = build_mac(gh_token=gh_token, debug=args.debug)
else:
- upgrade_sdist_dependencies()
test_makefile()
artifacts = build_sdist()
+ twine_check(artifacts)
upload_to_pypi = True
if args.upload:
diff --git a/scripts/dev/changelog_urls.json b/scripts/dev/changelog_urls.json
new file mode 100644
index 000000000..0de1d68d9
--- /dev/null
+++ b/scripts/dev/changelog_urls.json
@@ -0,0 +1,161 @@
+{
+ "pyparsing": "https://github.com/pyparsing/pyparsing/blob/master/CHANGES",
+ "pylint": "https://pylint.pycqa.org/en/latest/whatsnew/changelog.html",
+ "dill": "https://github.com/uqfoundation/dill/commits/master",
+ "isort": "https://pycqa.github.io/isort/CHANGELOG/",
+ "lazy-object-proxy": "https://github.com/ionelmc/python-lazy-object-proxy/blob/master/CHANGELOG.rst",
+ "mccabe": "https://github.com/PyCQA/mccabe#changes",
+ "pytest-cov": "https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst",
+ "pytest-xdist": "https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst",
+ "pytest-forked": "https://github.com/pytest-dev/pytest-forked/blob/master/CHANGELOG.rst",
+ "pytest-xvfb": "https://github.com/The-Compiler/pytest-xvfb/blob/master/CHANGELOG.rst",
+ "PyVirtualDisplay": "https://github.com/ponty/PyVirtualDisplay/commits/master",
+ "execnet": "https://execnet.readthedocs.io/en/latest/changelog.html",
+ "pytest-rerunfailures": "https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst",
+ "pytest-repeat": "https://github.com/pytest-dev/pytest-repeat/blob/master/CHANGES.rst",
+ "requests": "https://github.com/psf/requests/blob/master/HISTORY.md",
+ "requests-file": "https://github.com/dashea/requests-file/blob/master/CHANGES.rst",
+ "Werkzeug": "https://werkzeug.palletsprojects.com/en/latest/changes/",
+ "click": "https://click.palletsprojects.com/en/latest/changes/",
+ "itsdangerous": "https://itsdangerous.palletsprojects.com/en/latest/changes/",
+ "parse-type": "https://github.com/jenisys/parse_type/blob/master/CHANGES.txt",
+ "sortedcontainers": "https://github.com/grantjenks/python-sortedcontainers/blob/master/HISTORY.rst",
+ "soupsieve": "https://facelessuser.github.io/soupsieve/about/changelog/",
+ "Flask": "https://flask.palletsprojects.com/en/latest/changes/",
+ "Mako": "https://docs.makotemplates.org/en/latest/changelog.html",
+ "glob2": "https://github.com/miracle2k/python-glob2/blob/master/CHANGES",
+ "hypothesis": "https://hypothesis.readthedocs.io/en/latest/changes.html",
+ "mypy": "https://mypy-lang.blogspot.com/",
+ "types-PyYAML": "https://github.com/python/typeshed/commits/master/stubs/PyYAML",
+ "types-dataclasses": "https://github.com/python/typeshed/commits/master/stubs/dataclasses",
+ "pytest": "https://docs.pytest.org/en/latest/changelog.html",
+ "iniconfig": "https://github.com/RonnyPfannschmidt/iniconfig/blob/master/CHANGELOG",
+ "tox": "https://tox.readthedocs.io/en/latest/changelog.html",
+ "PyYAML": "https://github.com/yaml/pyyaml/blob/master/CHANGES",
+ "pytest-bdd": "https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst",
+ "snowballstemmer": "https://github.com/snowballstem/snowball/blob/master/NEWS",
+ "virtualenv": "https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst",
+ "packaging": "https://packaging.pypa.io/en/latest/changelog.html",
+ "build": "https://github.com/pypa/build/blob/main/CHANGELOG.rst",
+ "attrs": "https://www.attrs.org/en/stable/changelog.html",
+ "Jinja2": "https://jinja.palletsprojects.com/en/latest/changes/",
+ "MarkupSafe": "https://markupsafe.palletsprojects.com/en/latest/changes/",
+ "flake8": "https://github.com/PyCQA/flake8/tree/main/docs/source/release-notes",
+ "flake8-docstrings": "https://pypi.org/project/flake8-docstrings/",
+ "flake8-debugger": "https://github.com/JBKahn/flake8-debugger/",
+ "flake8-builtins": "https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst",
+ "flake8-bugbear": "https://github.com/PyCQA/flake8-bugbear#change-log",
+ "flake8-tidy-imports": "https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst",
+ "flake8-tuple": "https://github.com/ar4s/flake8_tuple/blob/master/HISTORY.rst",
+ "flake8-comprehensions": "https://github.com/adamchainz/flake8-comprehensions/blob/master/HISTORY.rst",
+ "flake8-copyright": "https://github.com/savoirfairelinux/flake8-copyright/blob/master/CHANGELOG.rst",
+ "flake8-deprecated": "https://github.com/gforcada/flake8-deprecated/blob/master/CHANGES.rst",
+ "flake8-future-import": "https://github.com/xZise/flake8-future-import#changes",
+ "flake8-mock": "https://github.com/aleGpereira/flake8-mock#changes",
+ "flake8-polyfill": "https://gitlab.com/pycqa/flake8-polyfill/-/blob/master/CHANGELOG.rst",
+ "flake8-string-format": "https://github.com/xZise/flake8-string-format#changes",
+ "flake8-plugin-utils": "https://github.com/afonasev/flake8-plugin-utils#change-log",
+ "flake8-pytest-style": "https://github.com/m-burst/flake8-pytest-style#change-log",
+ "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",
+ "cffi": "https://github.com/python-cffi/release-doc/blob/master/doc/source/whatsnew.rst",
+ "astroid": "https://github.com/PyCQA/astroid/blob/main/ChangeLog",
+ "pytest-instafail": "https://github.com/pytest-dev/pytest-instafail/blob/master/CHANGES.rst",
+ "coverage": "https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst",
+ "colorama": "https://github.com/tartley/colorama/blob/master/CHANGELOG.rst",
+ "hunter": "https://github.com/ionelmc/python-hunter/blob/master/CHANGELOG.rst",
+ "uritemplate": "https://github.com/python-hyper/uritemplate/blob/master/HISTORY.rst",
+ "more-itertools": "https://github.com/erikrose/more-itertools/blob/master/docs/versions.rst",
+ "pydocstyle": "https://www.pydocstyle.org/en/latest/release_notes.html",
+ "Sphinx": "https://www.sphinx-doc.org/en/master/changes.html",
+ "Babel": "https://github.com/python-babel/babel/blob/master/CHANGES",
+ "alabaster": "https://alabaster.readthedocs.io/en/latest/changelog.html",
+ "imagesize": "https://github.com/shibukawa/imagesize_py/commits/master",
+ "pytz": "https://mm.icann.org/pipermail/tz-announce/",
+ "sphinxcontrib-applehelp": "https://www.sphinx-doc.org/en/master/changes.html",
+ "sphinxcontrib-devhelp": "https://www.sphinx-doc.org/en/master/changes.html",
+ "sphinxcontrib-htmlhelp": "https://www.sphinx-doc.org/en/master/changes.html",
+ "sphinxcontrib-jsmath": "https://www.sphinx-doc.org/en/master/changes.html",
+ "sphinxcontrib-qthelp": "https://www.sphinx-doc.org/en/master/changes.html",
+ "sphinxcontrib-serializinghtml": "https://www.sphinx-doc.org/en/master/changes.html",
+ "jaraco.functools": "https://github.com/jaraco/jaraco.functools/blob/master/CHANGES.rst",
+ "parse": "https://github.com/r1chardj0n3s/parse#potential-gotchas",
+ "py": "https://py.readthedocs.io/en/latest/changelog.html#changelog",
+ "Pympler": "https://github.com/pympler/pympler/blob/master/CHANGELOG.md",
+ "pytest-mock": "https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst",
+ "pytest-qt": "https://github.com/pytest-dev/pytest-qt/blob/master/CHANGELOG.rst",
+ "pyinstaller": "https://pyinstaller.readthedocs.io/en/stable/CHANGES.html",
+ "pyinstaller-hooks-contrib": "https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/CHANGELOG.rst",
+ "pytest-benchmark": "https://pytest-benchmark.readthedocs.io/en/stable/changelog.html",
+ "typed-ast": "https://github.com/python/typed_ast/commits/master",
+ "docutils": "https://docutils.sourceforge.io/RELEASE-NOTES.html",
+ "bump2version": "https://github.com/c4urself/bump2version/blob/master/CHANGELOG.md",
+ "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://github.com/lxml/lxml/blob/master/CHANGES.txt",
+ "wrapt": "https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst",
+ "pep517": "https://github.com/pypa/pep517/blob/master/doc/changelog.rst",
+ "cryptography": "https://cryptography.io/en/latest/changelog.html",
+ "toml": "https://github.com/uiri/toml/releases",
+ "tomli": "https://github.com/hukkin/tomli/blob/master/CHANGELOG.md",
+ "PyQt5": "https://www.riverbankcomputing.com/news",
+ "PyQt5-Qt5": "https://www.riverbankcomputing.com/news",
+ "PyQtWebEngine": "https://www.riverbankcomputing.com/news",
+ "PyQtWebEngine-Qt5": "https://www.riverbankcomputing.com/news",
+ "PyQt-builder": "https://www.riverbankcomputing.com/news",
+ "PyQt5-sip": "https://www.riverbankcomputing.com/news",
+ "PyQt5-stubs": "https://github.com/stlehmann/PyQt5-stubs/blob/master/CHANGELOG.md",
+ "sip": "https://www.riverbankcomputing.com/news",
+ "Pygments": "https://pygments.org/docs/changelog/",
+ "vulture": "https://github.com/jendrikseipp/vulture/blob/master/CHANGELOG.md",
+ "distlib": "https://github.com/pypa/distlib/blob/master/CHANGES.rst",
+ "py-cpuinfo": "https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog",
+ "cheroot": "https://cheroot.cherrypy.dev/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/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",
+ "typing_extensions": "https://github.com/python/typing/blob/master/typing_extensions/CHANGELOG",
+ "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",
+ "pprintpp": "https://github.com/wolever/pprintpp/blob/master/CHANGELOG.txt",
+ "beautifulsoup4": "https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG",
+ "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/blob/main/docs/changelog.rst",
+ "github3.py": "https://github3.readthedocs.io/en/latest/release-notes/index.html",
+ "manhole": "https://github.com/ionelmc/python-manhole/blob/master/CHANGELOG.rst",
+ "pycparser": "https://github.com/eliben/pycparser/blob/master/CHANGES",
+ "python-dateutil": "https://dateutil.readthedocs.io/en/stable/changelog.html",
+ "platformdirs": "https://github.com/platformdirs/platformdirs/blob/main/CHANGES.rst",
+ "pluggy": "https://github.com/pytest-dev/pluggy/blob/master/CHANGELOG.rst",
+ "mypy-extensions": "https://github.com/python/mypy_extensions/commits/master",
+ "pyroma": "https://github.com/regebro/pyroma/blob/master/CHANGES.txt",
+ "adblock": "https://github.com/ArniDagur/python-adblock/blob/master/CHANGELOG.md",
+ "importlib-resources": "https://importlib-resources.readthedocs.io/en/latest/history.html",
+ "importlib-metadata": "https://github.com/python/importlib_metadata/blob/main/CHANGES.rst",
+ "zipp": "https://github.com/jaraco/zipp/blob/main/CHANGES.rst",
+ "dataclasses": "https://github.com/ericvsmith/dataclasses#release-history",
+ "pip": "https://pip.pypa.io/en/stable/news/",
+ "wheel": "https://wheel.readthedocs.io/en/stable/news.html",
+ "setuptools": "https://setuptools.readthedocs.io/en/latest/history.html",
+ "future": "https://python-future.org/whatsnew.html",
+ "pefile": "https://github.com/erocarrera/pefile/commits/master",
+ "SecretStorage": "https://github.com/mitya57/secretstorage/blob/master/changelog",
+ "bleach": "https://github.com/mozilla/bleach/blob/main/CHANGES",
+ "jeepney": "https://gitlab.com/takluyver/jeepney/-/blob/master/docs/release-notes.rst",
+ "keyring": "https://github.com/jaraco/keyring/blob/main/CHANGES.rst",
+ "pkginfo": "https://bazaar.launchpad.net/~tseaver/pkginfo/trunk/view/head:/CHANGES.txt",
+ "readme-renderer": "https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst",
+ "requests-toolbelt": "https://github.com/requests/toolbelt/blob/master/HISTORY.rst",
+ "rfc3986": "https://rfc3986.readthedocs.io/en/latest/release-notes/index.html",
+ "tqdm": "https://tqdm.github.io/releases/",
+ "twine": "https://twine.readthedocs.io/en/stable/changelog.html",
+ "webencodings": "https://github.com/gsnedders/python-webencodings/commits/master",
+ "PyJWT": "https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst"
+}
diff --git a/scripts/dev/gen_resources.py b/scripts/dev/gen_resources.py
index dd76c123e..efa6c6a3d 100644
--- a/scripts/dev/gen_resources.py
+++ b/scripts/dev/gen_resources.py
@@ -21,6 +21,10 @@
"""Generate Qt resources based on source files."""
import subprocess
+import pathlib
-with open('qutebrowser/resources.py', 'w', encoding='utf-8') as f:
- subprocess.run(['pyrcc5', 'qutebrowser.rcc'], stdout=f, check=True)
+ROOT = pathlib.Path(__file__).parents[2]
+OUTPUT = ROOT / 'qutebrowser' / 'resources.py'
+INPUT = ROOT / 'misc' / 'qutebrowser.rcc'
+
+subprocess.run(['pyrcc5', '-o', str(OUTPUT), str(INPUT)], check=True)
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index bae51e372..37c59d98f 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -111,7 +111,7 @@ def check_changelog_urls(_args: argparse.Namespace = None) -> bool:
utils.print_col(f"Extra changelog URLs: {req_str}", 'red')
if not ok:
- print("Hint: Changelog URLs are in scripts/dev/recompile_requirements.py")
+ print("Hint: Changelog URLs are in scripts/dev/changelog_urls.json")
return ok
@@ -199,11 +199,6 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]:
"Don't use a blanket 'noqa', use something like 'noqa: X123' instead.",
),
(
- re.compile(r'# type: ignore[^\[]'),
- ("Don't use a blanket 'type: ignore', use something like "
- "'type: ignore[error-code]' instead."),
- ),
- (
re.compile(r'# type: (?!ignore(\[|$))'),
"Don't use type comments, use type annotations instead.",
),
diff --git a/scripts/dev/pylint_checkers/qute_pylint/modeline.py b/scripts/dev/pylint_checkers/qute_pylint/modeline.py
index 114cfaf94..1df2c375e 100644
--- a/scripts/dev/pylint_checkers/qute_pylint/modeline.py
+++ b/scripts/dev/pylint_checkers/qute_pylint/modeline.py
@@ -31,9 +31,9 @@ class ModelineChecker(checkers.BaseChecker):
__implements__ = interfaces.IRawChecker
name = 'modeline'
- msgs = {'W9002': ('Does not have vim modeline', 'modeline-missing', None),
- 'W9003': ('Modeline is invalid', 'invalid-modeline', None),
- 'W9004': ('Modeline position is wrong', 'modeline-position', None)}
+ msgs = {'W9102': ('Does not have vim modeline', 'modeline-missing', None),
+ 'W9103': ('Modeline is invalid', 'invalid-modeline', None),
+ 'W9104': ('Modeline position is wrong', 'modeline-position', None)}
options = ()
priority = -1
diff --git a/scripts/dev/pylint_checkers/qute_pylint/openencoding.py b/scripts/dev/pylint_checkers/qute_pylint/openencoding.py
deleted file mode 100644
index 972a55db8..000000000
--- a/scripts/dev/pylint_checkers/qute_pylint/openencoding.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
-
-# This file is part of qutebrowser.
-#
-# qutebrowser is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# qutebrowser is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
-
-"""Make sure open() has an encoding set."""
-
-import astroid
-from pylint import interfaces, checkers
-from pylint.checkers import utils
-
-
-class OpenEncodingChecker(checkers.BaseChecker):
-
- """Checker to check open() has an encoding set."""
-
- __implements__ = interfaces.IAstroidChecker
- name = 'open-encoding'
-
- msgs = {
- 'W9400': ('open() called without encoding', 'open-without-encoding',
- None),
- }
-
- @utils.check_messages('open-without-encoding')
- def visit_call(self, node):
- """Visit a Call node."""
- if hasattr(node, 'func'):
- infer = utils.safe_infer(node.func)
- if infer and infer.root().name == '_io':
- if getattr(node.func, 'name', None) in ['open', 'file']:
- self._check_open_encoding(node)
-
- def _check_open_encoding(self, node):
- """Check that an open() call always has an encoding set."""
- try:
- mode_arg = utils.get_argument_from_call(node, position=1,
- keyword='mode')
- except utils.NoSuchArgumentError:
- mode_arg = None
- _encoding = None
- try:
- _encoding = utils.get_argument_from_call(node, position=2)
- except utils.NoSuchArgumentError:
- try:
- _encoding = utils.get_argument_from_call(node,
- keyword='encoding')
- except utils.NoSuchArgumentError:
- pass
- if _encoding is None:
- if mode_arg is None:
- mode = None
- else:
- mode = utils.safe_infer(mode_arg)
- if mode is not None and not isinstance(mode, astroid.Const):
- # We can't say what mode is exactly.
- return
- if mode is None:
- self.add_message('open-without-encoding', node=node)
- elif 'b' in getattr(mode, 'value', ''):
- # Files opened as binary don't need an encoding.
- return
- else:
- self.add_message('open-without-encoding', node=node)
-
-
-def register(linter):
- """Register this checker."""
- linter.register_checker(OpenEncodingChecker(linter))
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index 158741e5c..f8337b21f 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -24,9 +24,12 @@ import re
import sys
import os.path
import glob
+import json
import subprocess
import tempfile
import argparse
+import shutil
+import pathlib
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir))
@@ -37,157 +40,8 @@ REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'..', '..') # /scripts/dev -> /scripts -> /
REQ_DIR = os.path.join(REPO_DIR, 'misc', 'requirements')
-CHANGELOG_URLS = {
- 'pyparsing': 'https://github.com/pyparsing/pyparsing/blob/master/CHANGES',
- 'pylint': 'https://pylint.pycqa.org/en/latest/whatsnew/changelog.html',
- 'isort': 'https://pycqa.github.io/isort/CHANGELOG/',
- 'lazy-object-proxy': 'https://github.com/ionelmc/python-lazy-object-proxy/blob/master/CHANGELOG.rst',
- 'mccabe': 'https://github.com/PyCQA/mccabe#changes',
- 'pytest-cov': 'https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst',
- 'pytest-xdist': 'https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst',
- 'pytest-forked': 'https://github.com/pytest-dev/pytest-forked/blob/master/CHANGELOG',
- 'pytest-xvfb': 'https://github.com/The-Compiler/pytest-xvfb/blob/master/CHANGELOG.rst',
- 'EasyProcess': 'https://github.com/ponty/EasyProcess/commits/master',
- 'PyVirtualDisplay': 'https://github.com/ponty/PyVirtualDisplay/commits/master',
- 'execnet': 'https://execnet.readthedocs.io/en/latest/changelog.html',
- 'pytest-rerunfailures': 'https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst',
- 'pytest-repeat': 'https://github.com/pytest-dev/pytest-repeat/blob/master/CHANGES.rst',
- 'requests': 'https://github.com/psf/requests/blob/master/HISTORY.md',
- 'requests-file': 'https://github.com/dashea/requests-file/blob/master/CHANGES.rst',
- 'Werkzeug': 'https://werkzeug.palletsprojects.com/en/latest/changes/',
- 'click': 'https://click.palletsprojects.com/en/latest/changes/',
- 'itsdangerous': 'https://itsdangerous.palletsprojects.com/en/latest/changes/',
- 'parse-type': 'https://github.com/jenisys/parse_type/blob/master/CHANGES.txt',
- 'sortedcontainers': 'https://github.com/grantjenks/python-sortedcontainers/blob/master/HISTORY.rst',
- 'soupsieve': 'https://facelessuser.github.io/soupsieve/about/changelog/',
- 'Flask': 'https://flask.palletsprojects.com/en/latest/changes/',
- 'Mako': 'https://docs.makotemplates.org/en/latest/changelog.html',
- 'glob2': 'https://github.com/miracle2k/python-glob2/blob/master/CHANGES',
- 'hypothesis': 'https://hypothesis.readthedocs.io/en/latest/changes.html',
- 'mypy': 'https://mypy-lang.blogspot.com/',
- 'types-PyYAML': 'https://github.com/python/typeshed/commits/master/stubs/PyYAML',
- 'types-dataclasses': 'https://github.com/python/typeshed/commits/master/stubs/dataclasses',
- 'pytest': 'https://docs.pytest.org/en/latest/changelog.html',
- 'iniconfig': 'https://github.com/RonnyPfannschmidt/iniconfig/blob/master/CHANGELOG',
- 'tox': 'https://tox.readthedocs.io/en/latest/changelog.html',
- 'PyYAML': 'https://github.com/yaml/pyyaml/blob/master/CHANGES',
- 'pytest-bdd': 'https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst',
- 'snowballstemmer': 'https://github.com/snowballstem/snowball/blob/master/NEWS',
- 'virtualenv': 'https://virtualenv.pypa.io/en/latest/changelog.html',
- 'packaging': 'https://packaging.pypa.io/en/latest/changelog.html',
- 'build': 'https://github.com/pypa/build/blob/main/CHANGELOG.rst',
- 'attrs': 'https://www.attrs.org/en/stable/changelog.html',
- 'Jinja2': 'https://jinja.palletsprojects.com/en/latest/changes/',
- 'MarkupSafe': 'https://markupsafe.palletsprojects.com/en/latest/changes/',
- 'flake8': 'https://gitlab.com/pycqa/flake8/tree/master/docs/source/release-notes',
- 'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/',
- 'flake8-debugger': 'https://github.com/JBKahn/flake8-debugger/',
- 'flake8-builtins': 'https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst',
- 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear#change-log',
- 'flake8-tidy-imports': 'https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst',
- 'flake8-tuple': 'https://github.com/ar4s/flake8_tuple/blob/master/HISTORY.rst',
- 'flake8-comprehensions': 'https://github.com/adamchainz/flake8-comprehensions/blob/master/HISTORY.rst',
- 'flake8-copyright': 'https://github.com/savoirfairelinux/flake8-copyright/blob/master/CHANGELOG.rst',
- 'flake8-deprecated': 'https://github.com/gforcada/flake8-deprecated/blob/master/CHANGES.rst',
- 'flake8-future-import': 'https://github.com/xZise/flake8-future-import#changes',
- 'flake8-mock': 'https://github.com/aleGpereira/flake8-mock#changes',
- 'flake8-polyfill': 'https://gitlab.com/pycqa/flake8-polyfill/-/blob/master/CHANGELOG.rst',
- 'flake8-string-format': 'https://github.com/xZise/flake8-string-format#changes',
- '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',
- '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',
- 'coverage': 'https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst',
- 'colorama': 'https://github.com/tartley/colorama/blob/master/CHANGELOG.rst',
- 'hunter': 'https://github.com/ionelmc/python-hunter/blob/master/CHANGELOG.rst',
- 'uritemplate': 'https://github.com/python-hyper/uritemplate/blob/master/HISTORY.rst',
- 'more-itertools': 'https://github.com/erikrose/more-itertools/blob/master/docs/versions.rst',
- 'pydocstyle': 'https://www.pydocstyle.org/en/latest/release_notes.html',
- 'Sphinx': 'https://www.sphinx-doc.org/en/master/changes.html',
- 'Babel': 'https://github.com/python-babel/babel/blob/master/CHANGES',
- 'alabaster': 'https://alabaster.readthedocs.io/en/latest/changelog.html',
- 'imagesize': 'https://github.com/shibukawa/imagesize_py/commits/master',
- 'pytz': 'https://mm.icann.org/pipermail/tz-announce/',
- 'sphinxcontrib-applehelp': 'https://www.sphinx-doc.org/en/master/changes.html',
- 'sphinxcontrib-devhelp': 'https://www.sphinx-doc.org/en/master/changes.html',
- 'sphinxcontrib-htmlhelp': 'https://www.sphinx-doc.org/en/master/changes.html',
- 'sphinxcontrib-jsmath': 'https://www.sphinx-doc.org/en/master/changes.html',
- 'sphinxcontrib-qthelp': 'https://www.sphinx-doc.org/en/master/changes.html',
- 'sphinxcontrib-serializinghtml': 'https://www.sphinx-doc.org/en/master/changes.html',
- 'jaraco.functools': 'https://github.com/jaraco/jaraco.functools/blob/master/CHANGES.rst',
- 'parse': 'https://github.com/r1chardj0n3s/parse#potential-gotchas',
- 'py': 'https://py.readthedocs.io/en/latest/changelog.html#changelog',
- 'Pympler': 'https://github.com/pympler/pympler/blob/master/CHANGELOG.md',
- 'pytest-mock': 'https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst',
- 'pytest-qt': 'https://github.com/pytest-dev/pytest-qt/blob/master/CHANGELOG.rst',
- 'pyinstaller': 'https://pyinstaller.readthedocs.io/en/stable/CHANGES.html',
- 'pyinstaller-hooks-contrib': 'https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/CHANGELOG.rst',
- 'pytest-benchmark': 'https://pytest-benchmark.readthedocs.io/en/stable/changelog.html',
- 'typed-ast': 'https://github.com/python/typed_ast/commits/master',
- 'docutils': 'https://docutils.sourceforge.io/RELEASE-NOTES.html',
- 'bump2version': 'https://github.com/c4urself/bump2version/blob/master/CHANGELOG.md',
- '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',
- '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',
- 'cryptography': 'https://cryptography.io/en/latest/changelog.html',
- 'toml': 'https://github.com/uiri/toml/releases',
- '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',
- 'PyQt5-stubs': 'https://github.com/stlehmann/PyQt5-stubs/blob/master/CHANGELOG.md',
- 'sip': 'https://www.riverbankcomputing.com/news',
- 'Pygments': 'https://pygments.org/docs/changelog/',
- 'vulture': 'https://github.com/jendrikseipp/vulture/blob/master/CHANGELOG.md',
- 'distlib': 'https://bitbucket.org/pypa/distlib/src/master/CHANGES.rst',
- 'py-cpuinfo': 'https://github.com/workhorsy/py-cpuinfo/blob/master/ChangeLog',
- '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',
- 'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst',
- 'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md',
- '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',
- 'pprintpp': 'https://github.com/wolever/pprintpp/blob/master/CHANGELOG.txt',
- 'beautifulsoup4': 'https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG',
- '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/benediktschmitt/py-filelock/commits/master',
- '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',
- 'python-dateutil': 'https://dateutil.readthedocs.io/en/stable/changelog.html',
- 'appdirs': 'https://github.com/ActiveState/appdirs/blob/master/CHANGES.rst',
- 'pluggy': 'https://github.com/pytest-dev/pluggy/blob/master/CHANGELOG.rst',
- 'inflect': 'https://github.com/jazzband/inflect/blob/master/CHANGES.rst',
- 'jinja2-pluralize': 'https://github.com/audreyfeldroy/jinja2_pluralize/blob/master/HISTORY.rst',
- 'mypy-extensions': 'https://github.com/python/mypy_extensions/commits/master',
- 'pyroma': 'https://github.com/regebro/pyroma/blob/master/HISTORY.txt',
- 'adblock': 'https://github.com/ArniDagur/python-adblock/blob/master/CHANGELOG.md',
- 'importlib-resources': 'https://importlib-resources.readthedocs.io/en/latest/history.html',
- 'importlib-metadata': 'https://github.com/python/importlib_metadata/blob/main/CHANGES.rst',
- 'zipp': 'https://github.com/jaraco/zipp/blob/main/CHANGES.rst',
- 'dataclasses': 'https://github.com/ericvsmith/dataclasses#release-history',
- 'pip': 'https://pip.pypa.io/en/stable/news/',
- 'wheel': 'https://wheel.readthedocs.io/en/stable/news.html',
- 'setuptools': 'https://setuptools.readthedocs.io/en/latest/history.html',
- 'future': 'https://python-future.org/whatsnew.html',
- 'pefile': 'https://github.com/erocarrera/pefile/commits/master',
- 'Deprecated': 'https://github.com/tantale/deprecated/blob/master/CHANGELOG.rst',
-}
+CHANGELOG_URLS_PATH = pathlib.Path(__file__).parent / "changelog_urls.json"
+CHANGELOG_URLS = json.loads(CHANGELOG_URLS_PATH.read_text())
def convert_line(line, comments):
@@ -235,6 +89,7 @@ def read_comments(fobj):
'add': [],
'replace': {},
'pre': False,
+ 'pip_args': [],
}
for line in fobj:
if line.startswith('#@'):
@@ -264,6 +119,8 @@ def read_comments(fobj):
comments['add'].append(args)
elif command == 'pre':
comments['pre'] = True
+ elif command == 'pip_args':
+ comments['pip_args'] += args.split()
return comments
@@ -287,7 +144,7 @@ def run_pip(venv_dir, *args, quiet=False, **kwargs):
return subprocess.run([venv_python, '-m', 'pip'] + args, check=True, **kwargs)
-def init_venv(host_python, venv_dir, requirements, pre=False):
+def init_venv(host_python, venv_dir, requirements, pre=False, pip_args=None):
"""Initialize a new virtualenv and install the given packages."""
with utils.gha_group('Creating virtualenv'):
utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue')
@@ -299,6 +156,8 @@ def init_venv(host_python, venv_dir, requirements, pre=False):
install_command = ['install', '-r', requirements]
if pre:
install_command.append('--pre')
+ if pip_args:
+ install_command += pip_args
with utils.gha_group('Installing requirements'):
run_pip(venv_dir, *install_command)
@@ -329,10 +188,11 @@ class Change:
"""A single requirements change from a git diff output."""
- def __init__(self, name):
+ def __init__(self, name: str, base_path: pathlib.Path) -> None:
self.name = name
self.old = None
self.new = None
+ self.base = extract_requirement_name(base_path)
if CHANGELOG_URLS.get(name):
self.url = CHANGELOG_URLS[name]
self.link = '[{}]({})'.format(self.name, self.url)
@@ -340,24 +200,27 @@ class Change:
self.url = '(no changelog)'
self.link = self.name
+ def __lt__(self, other):
+ return (self.base, self.name.lower()) < (other.base, other.name.lower())
+
def __str__(self):
+ prefix = f"- [{self.base}] {self.name}"
+ suffix = f" {self.url}"
if self.old is None:
- return '- {} new: {} {}'.format(self.name, self.new, self.url)
+ return f"{prefix} new: {self.new} {suffix}"
elif self.new is None:
- return '- {} removed: {} {}'.format(self.name, self.old,
- self.url)
+ return f"{prefix} removed: {self.old} {suffix}"
else:
- return '- {} {} -> {} {}'.format(self.name, self.old, self.new,
- self.url)
+ return f"{prefix} {self.old} -> {self.new} {suffix}"
def table_str(self):
"""Generate a markdown table."""
if self.old is None:
- return '| {} | -- | {} |'.format(self.link, self.new)
+ return f'| {self.base} | {self.link} | -- | {self.new} |'
elif self.new is None:
- return '| {} | {} | -- |'.format(self.link, self.old)
+ return f'| {self.base} | {self.link} | {self.old} | -- |'
else:
- return '| {} | {} | {} |'.format(self.link, self.old, self.new)
+ return f'| {self.base} | {self.link} | {self.old} | {self.new} |'
def _get_changed_files():
@@ -365,25 +228,40 @@ def _get_changed_files():
changed_files = set()
filenames = git_diff('--name-only')
for filename in filenames:
- filename = filename.strip()
- filename = filename.replace('misc/requirements/requirements-', '')
- filename = filename.replace('.txt', '')
- changed_files.add(filename)
+ requirement_name = extract_requirement_name(pathlib.Path(filename))
+ changed_files.add(requirement_name)
return sorted(changed_files)
+def extract_requirement_name(path: pathlib.Path) -> str:
+ """Get a requirement name from a file path.
+
+ e.g. "pylint" from "misc/requirements/requirements-pylint.txt"
+ """
+ prefix = "requirements-"
+ assert path.suffix == ".txt", path
+ assert path.stem.startswith(prefix), path
+ return path.stem[len(prefix):]
+
+
def parse_versioned_line(line):
"""Parse a requirements.txt line into name/version."""
- if '==' in line:
- if line[0] == '#': # ignored dependency
- line = line[1:].strip()
+ if line[0] == '#': # ignored dependency
+ line = line[1:].strip()
+
+ # Strip comments and pip environment markers
+ line = line.rsplit('#', maxsplit=1)[0]
+ line = line.split(';')[0].strip()
- # Strip comments and pip environment markers
- line = line.rsplit('#', maxsplit=1)[0]
- line = line.split(';')[0].strip()
+ ops = ["==", "~=", "!=", ">", "<", ">=", "<="]
- name, version = line.split('==')
+ if any(op in line for op in ops):
+ # strictly speaking, this version isn't necessarily correct, but it's
+ # enough for the table.
+ for op in ops:
+ if op in line:
+ name, version = line.split(op)
elif line.startswith('-e'):
rest, name = line.split('#egg=')
version = rest.split('@')[1][:7]
@@ -400,10 +278,19 @@ def parse_versioned_line(line):
def _get_changes(diff):
"""Get a list of changed versions from git."""
changes_dict = {}
+ current_path = None
+
for line in diff:
if not line.startswith('-') and not line.startswith('+'):
continue
- elif line.startswith('+++ ') or line.startswith('--- '):
+ elif line.startswith('--- '):
+ prefix = '--- a/'
+ current_path = pathlib.Path(line[len(prefix):])
+ continue
+ elif line.startswith('+++ '):
+ prefix = '+++ b/'
+ new_path = pathlib.Path(line[len(prefix):])
+ assert current_path == new_path, (current_path, new_path)
continue
elif not line.strip():
# Could be newline changes on Windows
@@ -415,14 +302,14 @@ def _get_changes(diff):
name, version = parse_versioned_line(line[1:])
if name not in changes_dict:
- changes_dict[name] = Change(name)
+ changes_dict[name] = Change(name, base_path=current_path)
if line.startswith('-'):
changes_dict[name].old = version
elif line.startswith('+'):
changes_dict[name].new = version
- return [change for _name, change in sorted(changes_dict.items())]
+ return sorted(changes_dict.values())
def print_changed_files():
@@ -449,8 +336,8 @@ def print_changed_files():
print('::set-output name=changed::' +
files_text.replace('\n', '%0A'))
table_header = [
- '| Requirement | old | new |',
- '|-------------|-----|-----|',
+ '| File | Requirement | old | new |',
+ '|------|-------------|-----|-----|',
]
diff_table = '%0A'.join(table_header +
[change.table_str() for change in changes])
@@ -494,7 +381,8 @@ def build_requirements(name):
init_venv(host_python=host_python,
venv_dir=tmpdir,
requirements=filename,
- pre=comments['pre'])
+ pre=comments['pre'],
+ pip_args=comments['pip_args'])
with utils.gha_group('Freezing requirements'):
args = ['--all'] if name == 'tox' else []
proc = run_pip(tmpdir, 'freeze', *args, stdout=subprocess.PIPE)
@@ -553,9 +441,20 @@ def test_requirements(name, outfile, *, force=False):
print(f"Skipping test as there were no changes for {name}.")
return
+ in_file = os.path.join(REQ_DIR, 'requirements-{}.txt-raw'.format(name))
+ with open(in_file, 'r', encoding='utf-8') as f:
+ comments = read_comments(f)
+
host_python = get_host_python(name)
with tempfile.TemporaryDirectory() as tmpdir:
- init_venv(host_python, tmpdir, outfile)
+ init_venv(host_python, tmpdir, outfile, pip_args=comments['pip_args'])
+
+
+def cleanup_pylint_build():
+ """Clean up pylint_checkers build files."""
+ path = pathlib.Path(__file__).parent / 'pylint_checkers' / 'build'
+ utils.print_col(f'$ rm -r {path}', 'blue')
+ shutil.rmtree(path)
def main():
@@ -571,6 +470,8 @@ def main():
utils.print_title(name)
outfile = build_requirements(name)
test_requirements(name, outfile, force=args.force_test)
+ if name == 'pylint':
+ cleanup_pylint_build()
utils.print_title('Testing via tox')
if args.names and not args.force_test:
diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py
index d0385bd17..a44828a85 100644
--- a/scripts/dev/run_pylint_on_tests.py
+++ b/scripts/dev/run_pylint_on_tests.py
@@ -59,8 +59,14 @@ def main():
'len-as-condition',
'compare-to-empty-string',
'pointless-statement',
+ 'use-implicit-booleaness-not-comparison',
# directories without __init__.py...
'import-error',
+ # tests/helpers imports
+ 'wrong-import-order',
+ # https://github.com/PyCQA/pylint/issues/6036
+ # https://github.com/PyCQA/pylint/issues/6037
+ 'unnecessary-ellipsis',
]
toxinidir = sys.argv[1]
diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py
index 82bbdb167..febd2bf8a 100755
--- a/scripts/dev/src2asciidoc.py
+++ b/scripts/dev/src2asciidoc.py
@@ -444,7 +444,7 @@ def _generate_setting_option(f, opt):
if opt.restart:
f.write("\nThis setting requires a restart.\n")
if opt.supports_pattern:
- f.write("\nThis setting supports URL patterns.\n")
+ f.write("\nThis setting supports link:configuring{outfilesuffix}#patterns[URL patterns].\n")
if opt.no_autoconfig:
f.write("\nThis setting can only be set in config.py.\n")
_generate_setting_backend_info(f, opt)
@@ -536,7 +536,9 @@ def regenerate_manpage(filename):
# pylint: disable=protected-access
for group in parser._action_groups:
groupdata = []
- groupdata.append('=== {}'.format(group.title))
+ # https://bugs.python.org/issue9694 backport
+ title = "options" if group.title == "optional arguments" else group.title
+ groupdata.append('=== {}'.format(title))
if group.description is not None:
groupdata.append(group.description)
for action in group._group_actions:
diff --git a/scripts/dev/update_3rdparty.py b/scripts/dev/update_3rdparty.py
index 88f56b7f3..60b72d110 100755
--- a/scripts/dev/update_3rdparty.py
+++ b/scripts/dev/update_3rdparty.py
@@ -160,11 +160,11 @@ def test_dicts():
print('Testing dictionary {}... '.format(lang.code), end='')
lang_url = urllib.parse.urljoin(dictcli.API_URL, lang.remote_filename)
request = urllib.request.Request(lang_url, method='HEAD')
- response = urllib.request.urlopen(request)
- if response.status == 200:
- print('OK')
- else:
- print('ERROR: {}'.format(response.status))
+ with urllib.request.urlopen(request) as response:
+ if response.status == 200:
+ print('OK')
+ else:
+ print('ERROR: {}'.format(response.status))
def run(nsis=False, ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None,
diff --git a/scripts/dictcli.py b/scripts/dictcli.py
index 8cb93fb8a..a937fd31d 100755
--- a/scripts/dictcli.py
+++ b/scripts/dictcli.py
@@ -142,11 +142,11 @@ def parse_entry(entry):
def language_list_from_api():
"""Return a JSON with a list of available languages from Google API."""
listurl = API_URL + '?format=JSON'
- response = urllib.request.urlopen(listurl)
- # A special 5-byte prefix must be stripped from the response content
- # See: https://github.com/google/gitiles/issues/22
- # https://github.com/google/gitiles/issues/82
- json_content = response.read()[5:]
+ with urllib.request.urlopen(listurl) as response:
+ # A special 5-byte prefix must be stripped from the response content
+ # See: https://github.com/google/gitiles/issues/22
+ # https://github.com/google/gitiles/issues/82
+ json_content = response.read()[5:]
entries = json.loads(json_content.decode('utf-8'))['entries']
parsed_entries = [parse_entry(entry) for entry in entries]
return [entry for entry in parsed_entries if entry is not None]
@@ -176,8 +176,8 @@ def available_languages():
def download_dictionary(url, dest):
"""Download a decoded dictionary file."""
- response = urllib.request.urlopen(url)
- decoded = base64.decodebytes(response.read())
+ with urllib.request.urlopen(url) as response:
+ decoded = base64.decodebytes(response.read())
with open(dest, 'bw') as dict_file:
dict_file.write(decoded)
diff --git a/scripts/hostblock_blame.py b/scripts/hostblock_blame.py
index 38acaa58d..b18c62925 100644
--- a/scripts/hostblock_blame.py
+++ b/scripts/hostblock_blame.py
@@ -41,8 +41,8 @@ def main():
for url in configdata.DATA['content.blocking.hosts.lists'].default:
print("checking {}...".format(url))
- raw_file = urllib.request.urlopen(url)
- byte_io = io.BytesIO(raw_file.read())
+ with urllib.request.urlopen(url) as raw_file:
+ byte_io = io.BytesIO(raw_file.read())
f = hostblock.get_fileobj(byte_io)
for line in f:
line = line.decode('utf-8')
diff --git a/scripts/opengl_info.py b/scripts/opengl_info.py
new file mode 100644
index 000000000..1bfdf22aa
--- /dev/null
+++ b/scripts/opengl_info.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2020-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
+
+"""Show information about the OpenGL setup."""
+
+from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile,
+ QOffscreenSurface, QGuiApplication)
+
+app = QGuiApplication([])
+
+surface = QOffscreenSurface()
+surface.create()
+
+ctx = QOpenGLContext()
+ok = ctx.create()
+assert ok
+
+ok = ctx.makeCurrent(surface)
+assert ok
+
+print(f"GLES: {ctx.isOpenGLES()}")
+
+vp = QOpenGLVersionProfile()
+vp.setVersion(2, 0)
+
+vf = ctx.versionFunctions(vp)
+print(f"Vendor: {vf.glGetString(vf.GL_VENDOR)}")
+print(f"Renderer: {vf.glGetString(vf.GL_RENDERER)}")
+print(f"Version: {vf.glGetString(vf.GL_VERSION)}")
+print(f"Shading language version: {vf.glGetString(vf.GL_SHADING_LANGUAGE_VERSION)}")
+
+ctx.doneCurrent()
diff --git a/tests/conftest.py b/tests/conftest.py
index 40631af34..84cae784b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -17,31 +17,29 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
-# pylint: disable=unused-import,wildcard-import,unused-wildcard-import
-
"""The qutebrowser test suite conftest file."""
import os
import pathlib
import sys
-import warnings
import pytest
import hypothesis
-from PyQt5.QtCore import PYQT_VERSION
pytest.register_assert_rewrite('helpers')
+# pylint: disable=wildcard-import,unused-import,unused-wildcard-import
from helpers import logfail
from helpers.logfail import fail_on_logging
from helpers.messagemock import message_mock
from helpers.fixtures import * # noqa: F403
+# pylint: enable=wildcard-import,unused-import,unused-wildcard-import
from helpers import testutils
-from qutebrowser.utils import qtutils, standarddir, usertypes, utils, version
+from qutebrowser.utils import usertypes, utils, version
from qutebrowser.misc import objects, earlyinit
-from qutebrowser.qt import sip
-import qutebrowser.app # To register commands
+# To register commands
+import qutebrowser.app # pylint: disable=unused-import
_qute_scheme_handler = None
@@ -196,9 +194,8 @@ def pytest_ignore_collect(path):
@pytest.fixture(scope='session')
def qapp_args():
"""Make QtWebEngine unit tests run on older Qt versions + newer kernels."""
- seccomp_args = testutils.seccomp_args(qt_flag=False)
- if seccomp_args:
- return [sys.argv[0]] + seccomp_args
+ if testutils.disable_seccomp_bpf_sandbox():
+ return [sys.argv[0], testutils.DISABLE_SECCOMP_BPF_FLAG]
return []
@@ -214,20 +211,76 @@ 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
+ # pylint: disable=unused-import
+ if backend == 'webkit':
+ import PyQt5.QtWebKitWidgets
+ elif backend == 'webengine':
+ import PyQt5.QtWebEngineWidgets
+ else:
+ raise utils.Unreachable(backend)
+
+ return backend
+
+
+def _auto_select_backend():
+ # pylint: disable=unused-import
+ 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', ''):
@@ -285,7 +338,7 @@ def check_yaml_c_exts():
https://github.com/yaml/pyyaml/issues/416
"""
if testutils.ON_CI and sys.version_info[:2] != (3, 10):
- from yaml import CLoader
+ from yaml import CLoader # pylint: disable=unused-import
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py
index a4a089cea..14ac6f395 100644
--- a/tests/end2end/conftest.py
+++ b/tests/end2end/conftest.py
@@ -17,12 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
-# pylint: disable=unused-import
-
"""Things needed for end2end testing."""
import re
-import os
import pathlib
import sys
import shutil
@@ -34,13 +31,14 @@ from PyQt5.QtCore import PYQT_VERSION, QCoreApplication
pytest.register_assert_rewrite('end2end.fixtures')
+# pylint: disable=unused-import
from end2end.fixtures.notificationserver import notification_server
from end2end.fixtures.webserver import server, server_per_test, server2, ssl_server
from end2end.fixtures.quteprocess import (quteproc_process, quteproc,
quteproc_new)
from end2end.fixtures.testprocess import pytest_runtest_makereport
+# pylint: enable=unused-import
from qutebrowser.utils import qtutils, utils
-from qutebrowser.browser.webengine import spell
def pytest_configure(config):
@@ -61,7 +59,7 @@ def pytest_unconfigure(config):
stats.dump_stats((pathlib.Path('prof') / 'combined.pstats'))
-def _check_hex_version(op_str, running_version, version):
+def _check_version(op_str, running_version, version_str, as_hex=False):
operators = {
'==': operator.eq,
'!=': operator.ne,
@@ -71,9 +69,12 @@ def _check_hex_version(op_str, running_version, version):
'<': operator.lt,
}
op = operators[op_str]
- major, minor, patch = [int(e) for e in version.split('.')]
- hex_version = (major << 16) | (minor << 8) | patch
- return op(running_version, hex_version)
+ major, minor, patch = [int(e) for e in version_str.split('.')]
+ if as_hex:
+ version = (major << 16) | (minor << 8) | patch
+ else:
+ version = (major, minor, patch)
+ return op(running_version, version)
def _get_version_tag(tag):
@@ -84,7 +85,7 @@ def _get_version_tag(tag):
casesinto an appropriate @pytest.mark.skip marker, and falls back to
"""
version_re = re.compile(r"""
- (?P<package>qt|pyqt|pyqtwebengine)
+ (?P<package>qt|pyqt|pyqtwebengine|python)
(?P<operator>==|>=|!=|<)
(?P<version>\d+\.\d+(\.\d+)?)
""", re.VERBOSE)
@@ -108,10 +109,11 @@ def _get_version_tag(tag):
return pytest.mark.skipif(do_skip[op], reason='Needs ' + tag)
elif package == 'pyqt':
return pytest.mark.skipif(
- not _check_hex_version(
+ not _check_version(
op_str=match.group('operator'),
running_version=PYQT_VERSION,
- version=version
+ version_str=version,
+ as_hex=True,
),
reason='Needs ' + tag,
)
@@ -123,10 +125,21 @@ def _get_version_tag(tag):
else:
running_version = PYQT_WEBENGINE_VERSION
return pytest.mark.skipif(
- not _check_hex_version(
+ not _check_version(
+ op_str=match.group('operator'),
+ running_version=running_version,
+ version_str=version,
+ as_hex=True,
+ ),
+ reason='Needs ' + tag,
+ )
+ elif package == 'python':
+ running_version = sys.version_info
+ return pytest.mark.skipif(
+ not _check_version(
op_str=match.group('operator'),
running_version=running_version,
- version=version
+ version_str=version,
),
reason='Needs ' + tag,
)
@@ -165,7 +178,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/brave-adblock/generate.py b/tests/end2end/data/brave-adblock/generate.py
index 7a23a21ab..393cda4e7 100644
--- a/tests/end2end/data/brave-adblock/generate.py
+++ b/tests/end2end/data/brave-adblock/generate.py
@@ -53,9 +53,9 @@ def main():
else:
request = urllib.request.Request(URL)
print(f"Downloading {URL} ...")
- response = urllib.request.urlopen(request)
- assert response.status == 200
- data_str = response.read().decode("utf-8")
+ with urllib.request.urlopen(request) as response:
+ assert response.status == 200
+ data_str = response.read().decode("utf-8")
print(f"Saving to cache file {CACHE_PATH} ...")
CACHE_PATH.write_text(data_str, encoding="utf-8")
data = io.StringIO(data_str)
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/data/hints/html/tabindex-negative.html b/tests/end2end/data/hints/html/tabindex-negative.html
new file mode 100644
index 000000000..03adb32bf
--- /dev/null
+++ b/tests/end2end/data/hints/html/tabindex-negative.html
@@ -0,0 +1,13 @@
+<!-- target: null -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Span with tabindex -1</title>
+ </head>
+ <body>
+ <p>This text should not get a hint:</p>
+ <span tabindex=-1>test</span>
+ </body>
+</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/notifications.feature b/tests/end2end/features/notifications.feature
index 08c5f725f..cd34793ef 100644
--- a/tests/end2end/features/notifications.feature
+++ b/tests/end2end/features/notifications.feature
@@ -4,7 +4,8 @@ Feature: Notifications
HTML5 notification API interaction
Background:
- Given I open data/javascript/notifications.html
+ Given I clean up open tabs
+ And I open data/javascript/notifications.html
And I set content.notifications.enabled to true
And I run :click-element id button
And I clean up the notification server
@@ -120,8 +121,13 @@ Feature: Notifications
Scenario: User clicks presented notification
When I run :click-element id show-button
And I wait for the javascript message "notification shown"
+ And I open about:blank in a new tab
And I click the notification
Then the javascript message "notification clicked" should be logged
+ And the following tabs should be open:
+ - about:blank
+ - data/javascript/notifications.html (active)
+ - about:blank
@pyqtwebengine<5.15.0
Scenario: User clicks presented notification (old Qt)
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 e8fc9e7bc..54e84a10e 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
@@ -653,7 +674,7 @@ Feature: Tab management
Scenario: :tab-clone with -b and -w
When I run :tab-clone -b -w
- Then the error "Only one of -b/-w can be given!" should be shown.
+ Then the error "Only one of -b/-w/-p can be given!" should be shown.
Scenario: Cloning a tab with history and title
When I open data/title.html
@@ -737,6 +758,26 @@ Feature: Tab management
- url: http://localhost:*/data/title.html
title: Test title
+ Scenario: Cloning to private window
+ When I open data/title.html
+ And I run :tab-clone -p
+ And I wait until data/title.html is loaded
+ Then the session should look like:
+ windows:
+ - tabs:
+ - active: true
+ history:
+ - url: about:blank
+ - url: http://localhost:*/data/title.html
+ title: Test title
+ - private: true
+ tabs:
+ - active: true
+ history:
+ - url: about:blank
+ - url: http://localhost:*/data/title.html
+ title: Test title
+
# https://github.com/qutebrowser/qutebrowser/issues/2289
@qtwebkit_skip
@@ -1329,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
@@ -1386,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)
@@ -1663,3 +1741,22 @@ Feature: Tab management
And I run :undo
And I run :message-info "Still alive!"
Then the message "Still alive!" should be shown
+
+ Scenario: Passthrough mode override
+ When I run :set -u localhost:*/data/numbers/1.txt input.mode_override 'passthrough'
+ And I open data/numbers/1.txt
+ Then "Entering mode KeyMode.passthrough (reason: mode_override)" should be logged
+
+ Scenario: Insert mode override
+ When I run :set -u localhost:*/data/numbers/1.txt input.mode_override 'insert'
+ And I open data/numbers/1.txt
+ Then "Entering mode KeyMode.insert (reason: mode_override)" should be logged
+
+ Scenario: Mode override on tab switch
+ When I run :set -u localhost:*/data/numbers/1.txt input.mode_override 'insert'
+ And I open data/numbers/1.txt
+ And I wait for "Entering mode KeyMode.insert (reason: mode_override)" in the log
+ And I run :fake-key -g <esc>
+ And I open data/numbers/2.txt in a new tab
+ And I run :tab-prev
+ Then "Entering mode KeyMode.insert (reason: mode_override)" should be logged
diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature
index bb2b3e2fb..e8172ae20 100644
--- a/tests/end2end/features/utilcmds.feature
+++ b/tests/end2end/features/utilcmds.feature
@@ -93,6 +93,7 @@ Feature: Miscellaneous utility commands exposed to the user.
## :debug-cache-stats
+ @python>=3.9.0
Scenario: :debug-cache-stats
When I run :debug-cache-stats
Then "is_valid_prefix: CacheInfo(*)" should be logged
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index 3cbea01ad..ab8f28d26 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -48,10 +48,7 @@ instance_counter = itertools.count()
def is_ignored_qt_message(pytestconfig, message):
"""Check if the message is listed in qt_log_ignore."""
regexes = pytestconfig.getini('qt_log_ignore')
- for regex in regexes:
- if re.search(regex, message):
- return True
- return False
+ return any(re.search(regex, message) for regex in regexes)
def is_ignored_lowlevel_message(message):
@@ -552,8 +549,8 @@ class QuteProc(testprocess.Process):
'--debug-flag', 'werror', '--debug-flag',
'test-notification-service']
- if self.request.config.webengine:
- args += testutils.seccomp_args(qt_flag=True)
+ if self.request.config.webengine and testutils.disable_seccomp_bpf_sandbox():
+ args += testutils.DISABLE_SECCOMP_BPF_ARGS
args.append('about:blank')
return args
@@ -609,8 +606,7 @@ class QuteProc(testprocess.Process):
self.wait_for(category='webview',
message='Scroll position changed to ' + point)
- def wait_for(self, timeout=None, # pylint: disable=arguments-differ
- **kwargs):
+ def wait_for(self, timeout=None, **kwargs):
"""Extend wait_for to add divisor if a test is xfailing."""
__tracebackhide__ = (lambda e:
e.errisinstance(testprocess.WaitForTimeout))
@@ -716,7 +712,7 @@ class QuteProc(testprocess.Process):
target_arg)
self._wait_for_ipc()
- def start(self, *args, **kwargs): # pylint: disable=arguments-differ
+ def start(self, *args, **kwargs):
try:
super().start(*args, **kwargs)
except testprocess.ProcessExited:
@@ -913,8 +909,8 @@ class QuteProc(testprocess.Process):
"""Get a screenshot of the current page.
Arguments:
- probe: If given, only continue if the pixel at the given position isn't
- black (or whatever is specified by probe_color).
+ probe_pos: If given, only continue if the pixel at the given
+ position isn't black (or whatever is specified by probe_color).
"""
for _ in range(5):
tmp_path = self.request.getfixturevalue('tmp_path')
diff --git a/tests/end2end/fixtures/test_webserver.py b/tests/end2end/fixtures/test_webserver.py
index 3c825e5bc..ed0c32ae5 100644
--- a/tests/end2end/fixtures/test_webserver.py
+++ b/tests/end2end/fixtures/test_webserver.py
@@ -37,7 +37,8 @@ def test_server(server, qtbot, path, content, expected):
with qtbot.wait_signal(server.new_request, timeout=100):
url = 'http://localhost:{}{}'.format(server.port, path)
try:
- response = urllib.request.urlopen(url)
+ with urllib.request.urlopen(url) as response:
+ data = response.read().decode('utf-8')
except urllib.error.HTTPError as e:
# "Though being an exception (a subclass of URLError), an HTTPError
# can also function as a non-exceptional file-like return value
@@ -46,8 +47,6 @@ def test_server(server, qtbot, path, content, expected):
print(e.read().decode('utf-8'))
raise
- data = response.read().decode('utf-8')
-
assert server.get_requests() == [server.ExpectedRequest('GET', path)]
assert (content in data) == expected
diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py
index 0fc32cd88..2c2eab930 100644
--- a/tests/end2end/fixtures/webserver.py
+++ b/tests/end2end/fixtures/webserver.py
@@ -149,11 +149,9 @@ class WebserverProcess(testprocess.Process):
def _random_port(self) -> int:
"""Get a random free port."""
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.bind(('localhost', 0))
- port = sock.getsockname()[1]
- sock.close()
- return port
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+ sock.bind(('localhost', 0))
+ return sock.getsockname()[1]
def get_requests(self):
"""Get the requests to the server during this test."""
@@ -162,7 +160,7 @@ class WebserverProcess(testprocess.Process):
def _parse_line(self, line):
self._log(line)
- started_re = re.compile(r' \* Running on https?://127\.0\.0\.1:{}/ '
+ started_re = re.compile(r' \* Running on https?://127\.0\.0\.1:{}/? '
r'\(Press CTRL\+C to quit\)'.format(self.port))
if started_re.fullmatch(line):
self.ready.emit()
diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py
index 92f3091d1..b4964d973 100644
--- a/tests/end2end/fixtures/webserver_sub.py
+++ b/tests/end2end/fixtures/webserver_sub.py
@@ -292,6 +292,8 @@ def view_user_agent():
@app.route('/favicon.ico')
def favicon():
+ # WORKAROUND for https://github.com/PyCQA/pylint/issues/5783
+ # pylint: disable-next=no-member,useless-suppression
icon_dir = END2END_DIR.parents[1] / 'icons'
return flask.send_from_directory(
icon_dir, 'qutebrowser.ico', mimetype='image/vnd.microsoft.icon')
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/end2end/test_insert_mode.py b/tests/end2end/test_insert_mode.py
index e829f25a6..47e481b3d 100644
--- a/tests/end2end/test_insert_mode.py
+++ b/tests/end2end/test_insert_mode.py
@@ -22,7 +22,7 @@
import pytest
-@pytest.mark.parametrize(['file_name', 'elem_id', 'source', 'input_text'], [
+@pytest.mark.parametrize('file_name, elem_id, source, input_text', [
('textarea.html', 'qute-textarea', 'clipboard', 'qutebrowser'),
('textarea.html', 'qute-textarea', 'keypress', 'superqutebrowser'),
('input.html', 'qute-input', 'clipboard', 'amazingqutebrowser'),
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
index b860feed0..43809cfd4 100644
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -56,8 +56,8 @@ def _base_args(config):
else:
args += ['--backend', 'webkit']
- if config.webengine:
- args += testutils.seccomp_args(qt_flag=True)
+ if config.webengine and testutils.disable_seccomp_bpf_sandbox():
+ args += testutils.DISABLE_SECCOMP_BPF_ARGS
args.append('about:blank')
return args
@@ -826,3 +826,81 @@ def test_json_logging_without_debug(request, quteproc_new, runtime_tmpdir):
quteproc_new.exit_expected = True
quteproc_new.start(args, env={'XDG_RUNTIME_DIR': str(runtime_tmpdir)})
assert not quteproc_new.is_running()
+
+
+@pytest.mark.qtwebkit_skip
+@pytest.mark.parametrize(
+ 'sandboxing, has_namespaces, has_seccomp, has_yama, expected_result', [
+ ('enable-all', True, True, True, "You are adequately sandboxed."),
+ ('disable-seccomp-bpf', True, False, True, "You are NOT adequately sandboxed."),
+ ('disable-all', False, False, False, "You are NOT adequately sandboxed."),
+ ]
+)
+def test_sandboxing(
+ request, quteproc_new, sandboxing,
+ has_namespaces, has_seccomp, has_yama, expected_result,
+):
+ if not request.config.webengine:
+ pytest.skip("Skipped with QtWebKit")
+ elif sandboxing == "enable-all" and testutils.disable_seccomp_bpf_sandbox():
+ pytest.skip("Full sandboxing not supported")
+
+ args = _base_args(request.config) + [
+ '--temp-basedir',
+ '-s', 'qt.chromium.sandboxing', sandboxing,
+ ]
+ quteproc_new.start(args)
+
+ quteproc_new.open_url('chrome://sandbox')
+ text = quteproc_new.get_content()
+ print(text)
+
+ not_found_msg = ("The webpage at chrome://sandbox/ might be temporarily down or "
+ "it may have moved permanently to a new web address.")
+ if not_found_msg in text.split("\n"):
+ line = quteproc_new.wait_for(message='Load error: ERR_INVALID_URL')
+ line.expected = True
+ pytest.skip("chrome://sandbox/ not supported")
+
+ bpf_text = "Seccomp-BPF sandbox"
+ yama_text = "Ptrace Protection with Yama LSM"
+
+ if "\n\n\n" in text:
+ # Qt 5.12
+ header, rest = text.split("\n", maxsplit=1)
+ rest, result = rest.rsplit("\n\n", maxsplit=1)
+ lines = rest.replace("\t\n", "\t").split("\n\n\n")
+
+ expected_status = {
+ "Namespace Sandbox": "Yes" if has_namespaces else "No",
+ "Network namespaces": "Yes" if has_namespaces else "No",
+ "PID namespaces": "Yes" if has_namespaces else "No",
+ "SUID Sandbox": "No",
+
+ bpf_text: "Yes" if has_seccomp else "No",
+ f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No",
+
+ "Yama LSM Enforcing": "Yes" if has_yama else "No",
+ }
+ else:
+ header, *lines, empty, result = text.split("\n")
+ assert not empty
+
+ expected_status = {
+ "Layer 1 Sandbox": "Namespace" if has_namespaces else "None",
+
+ "PID namespaces": "Yes" if has_namespaces else "No",
+ "Network namespaces": "Yes" if has_namespaces else "No",
+
+ bpf_text: "Yes" if has_seccomp else "No",
+ f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No",
+
+ f"{yama_text} (Broker)": "Yes" if has_yama else "No",
+ f"{yama_text} (Non-broker)": "No",
+ }
+
+ assert header == "Sandbox Status"
+ assert result == expected_result
+
+ status = dict(line.split("\t") for line in lines)
+ assert status == expected_status
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index cd3778b8a..cc362290f 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -186,7 +186,7 @@ def testdata_scheme(qapp):
pass
@qutescheme.add_handler('testdata')
- def handler(url): # pylint: disable=unused-variable
+ def handler(url):
file_abs = os.path.abspath(os.path.dirname(__file__))
filename = os.path.join(file_abs, os.pardir, 'end2end',
url.path().lstrip('/'))
@@ -632,7 +632,7 @@ def redirect_webengine_data(data_tmpdir, monkeypatch):
monkeypatch.setenv('HOME', str(data_tmpdir))
-@pytest.fixture()
+@pytest.fixture
def short_tmpdir():
"""A short temporary directory for a XDG_RUNTIME_DIR."""
with tempfile.TemporaryDirectory() as tdir:
diff --git a/tests/helpers/logfail.py b/tests/helpers/logfail.py
index ae4ac9bc0..15d5a9253 100644
--- a/tests/helpers/logfail.py
+++ b/tests/helpers/logfail.py
@@ -39,8 +39,7 @@ class LogFailHandler(logging.Handler):
if logger.name == 'messagemock':
return
- if (logger.level == record.levelno or
- root_logger.level == record.levelno):
+ if record.levelno in (logger.level, root_logger.level):
# caplog.at_level(...) was used with the level of this message,
# i.e. it was expected.
return
diff --git a/tests/helpers/testutils.py b/tests/helpers/testutils.py
index 8bb622133..c607718ab 100644
--- a/tests/helpers/testutils.py
+++ b/tests/helpers/testutils.py
@@ -31,14 +31,9 @@ import importlib.machinery
import pytest
-from PyQt5.QtCore import qVersion
from PyQt5.QtGui import QColor
-try:
- from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION_STR
-except ImportError:
- PYQT_WEBENGINE_VERSION_STR = None
-from qutebrowser.utils import qtutils, log, utils
+from qutebrowser.utils import qtutils, log, utils, version
ON_CI = 'CI' in os.environ
@@ -267,35 +262,38 @@ def easyprivacy_txt():
return _decompress_gzip_datafile("easyprivacy.txt.gz")
-def seccomp_args(qt_flag):
- """Get necessary flags to disable the seccomp BPF sandbox.
+DISABLE_SECCOMP_BPF_FLAG = "--disable-seccomp-filter-sandbox"
+DISABLE_SECCOMP_BPF_ARGS = ["-s", "qt.chromium.sandboxing", "disable-seccomp-bpf"]
+
+
+def disable_seccomp_bpf_sandbox():
+ """Check whether we need to disable the seccomp BPF sandbox.
This is needed for some QtWebEngine setups, with older Qt versions but
newer kernels.
-
- Args:
- qt_flag: Add a '--qt-flag' argument.
"""
+ try:
+ from PyQt5 import QtWebEngine # pylint: disable=unused-import
+ except ImportError:
+ # no QtWebEngine available
+ return False
+
affected_versions = set()
for base, patch_range in [
- # 5.12.0 to 5.12.7 (inclusive)
- ('5.12', range(0, 8)),
+ # 5.12.0 to 5.12.10 (inclusive)
+ ('5.12', range(0, 11)),
# 5.13.0 to 5.13.2 (inclusive)
('5.13', range(0, 3)),
# 5.14.0
('5.14', [0]),
+ # 5.15.0 to 5.15.2 (inclusive)
+ ('5.15', range(0, 3)),
]:
for patch in patch_range:
- affected_versions.add('{}.{}'.format(base, patch))
-
- version = (PYQT_WEBENGINE_VERSION_STR
- if PYQT_WEBENGINE_VERSION_STR is not None
- else qVersion())
- if version in affected_versions:
- disable_arg = 'disable-seccomp-filter-sandbox'
- return ['--qt-flag', disable_arg] if qt_flag else ['--' + disable_arg]
+ affected_versions.add(utils.VersionNumber.parse(f'{base}.{patch}'))
- return []
+ versions = version.qtwebengine_versions(avoid_init=True)
+ return versions.webengine in affected_versions
def import_userscript(name):
diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py
index 77e648eed..b6650078c 100644
--- a/tests/unit/api/test_cmdutils.py
+++ b/tests/unit/api/test_cmdutils.py
@@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
-# pylint: disable=unused-variable
-
"""Tests for qutebrowser.api.cmdutils."""
import sys
@@ -292,7 +290,6 @@ class TestRegister:
class Enum(enum.Enum):
- # pylint: disable=invalid-name
x = enum.auto()
y = enum.auto()
diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py
index 288471ea0..86014040d 100644
--- a/tests/unit/browser/test_caret.py
+++ b/tests/unit/browser/test_caret.py
@@ -70,7 +70,7 @@ class Selection:
self._qtbot.wait(50)
- assert False, 'Failed to get selection!'
+ pytest.fail('Failed to get selection!')
def check_multiline(self, expected, *, strip=False):
self.check(textwrap.dedent(expected).strip(), strip=strip)
diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py
index 7906d385c..51a9effb8 100644
--- a/tests/unit/browser/test_history.py
+++ b/tests/unit/browser/test_history.py
@@ -141,7 +141,7 @@ class TestDelete:
class TestAdd:
- @pytest.fixture()
+ @pytest.fixture
def mock_time(self, mocker):
m = mocker.patch('qutebrowser.browser.history.time')
m.time.return_value = 12345
diff --git a/tests/unit/browser/test_inspector.py b/tests/unit/browser/test_inspector.py
index f7f532050..61ac9510d 100644
--- a/tests/unit/browser/test_inspector.py
+++ b/tests/unit/browser/test_inspector.py
@@ -151,4 +151,5 @@ def test_detach_after_toggling(hidden_again, needs_recreate,
else:
with qtbot.assert_not_emitted(fake_inspector.recreate):
fake_inspector.set_position(inspector.Position.window)
- assert fake_inspector.isVisible() and fake_inspector.isWindow()
+ assert fake_inspector.isVisible()
+ assert fake_inspector.isWindow()
diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py
index 593896e96..f7cc3e8c2 100644
--- a/tests/unit/browser/webkit/test_webkitelem.py
+++ b/tests/unit/browser/webkit/test_webkitelem.py
@@ -181,13 +181,30 @@ class SelectionAndFilterTests:
# We can't easily test <frame>/<iframe> as they vanish when setting
# them via QWebFrame::setHtml...
+ ('<img src="bar">', ['all', 'images', 'url']),
+ ('<summary>bar</summary>', ['all']),
+ ('<link />', ['all']),
+
+ ('<p contenteditable />', ['all', 'inputs']),
+ ('<p contenteditable="false" />', []),
('<p onclick="foo" foo="bar"/>', ['all']),
('<p onmousedown="foo" foo="bar"/>', ['all']),
('<p role="option" foo="bar"/>', ['all']),
+ ('<p role="tab" foo="bar"/>', ['all']),
+ ('<p role="checkbox" foo="bar"/>', ['all']),
+ ('<p role="menuitem" foo="bar"/>', ['all']),
+ ('<p role="menuitemcheckbox" foo="bar"/>', ['all']),
+ ('<p role="menuitemradio" foo="bar"/>', ['all']),
('<p role="button" foo="bar"/>', ['all']),
('<p role="button" href="bar"/>', ['all', 'url']),
- ('<span tabindex=0 />', ['all']),
+ ('<span tabindex="0" />', ['all']),
+ ('<span tabindex="-1" />', []),
+
+ ('<span ng-click=""></span>', ['all']),
+ ('<span ngClick=""></span>', ['all']),
+ ('<span data-ng-click=""></span>', ['all']),
+ ('<span x-ng-click=""></span>', ['all']),
]
GROUPS = ['all', 'links', 'images', 'url', 'inputs']
@@ -849,8 +866,6 @@ class TestIsEditable:
@pytest.mark.parametrize('setting, tagname, attributes, editable', [
(True, 'embed', {}, True),
- (True, 'embed', {}, True),
- (False, 'applet', {}, False),
(False, 'applet', {}, False),
(True, 'object', {'type': 'application/foo'}, True),
(False, 'object', {'type': 'application/foo'}, False),
diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py
index 89390cbf1..074228332 100644
--- a/tests/unit/completion/test_completionwidget.py
+++ b/tests/unit/completion/test_completionwidget.py
@@ -37,13 +37,13 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
mocker.patch('qutebrowser.completion.completer.Completer', autospec=True)
mocker.patch(
'qutebrowser.completion.completiondelegate.CompletionItemDelegate',
- new=lambda *_: None)
+ return_value=None)
view = completionwidget.CompletionView(cmd=status_command_stub, win_id=0)
qtbot.add_widget(view)
return view
-@pytest.fixture()
+@pytest.fixture
def model():
return completionmodel.CompletionModel()
@@ -163,16 +163,7 @@ def test_completion_item_focus_no_model(which, completionview, model, qtbot):
@pytest.mark.skip("Seems to disagree with reality, see #5897")
def test_completion_item_focus_fetch(completionview, model, qtbot):
- """Test that on_next_prev_item moves the selection properly.
-
- Args:
- which: the direction in which to move the selection.
- tree: Each list represents a completion category, with each string
- being an item under that category.
- expected: expected argument from on_selection_changed for each
- successive movement. None implies no signal should be
- emitted.
- """
+ """Test that on_next_prev_item moves the selection properly."""
cat = mock.Mock(spec=[
'layoutChanged', 'layoutAboutToBeChanged', 'canFetchMore',
'fetchMore', 'rowCount', 'index', 'data'])
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index c20fe293c..2c00acf68 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -76,7 +76,7 @@ def _check_completions(model, expected):
assert sum(model.column_widths) == 100
-@pytest.fixture()
+@pytest.fixture
def cmdutils_stub(monkeypatch, stubs):
"""Patch the cmdutils module to provide fake commands."""
return monkeypatch.setattr(objects, 'commands', {
@@ -93,7 +93,7 @@ def cmdutils_stub(monkeypatch, stubs):
})
-@pytest.fixture()
+@pytest.fixture
def configdata_stub(config_stub, monkeypatch, configdata_init):
"""Patch the configdata module to provide fake data."""
monkeypatch.setattr(configdata, 'DATA', collections.OrderedDict([
@@ -867,6 +867,34 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, win_registry,
QUrl('https://duckduckgo.com')]
+def test_tab_focus_completion_delete(qtmodeltester, fake_web_tab, win_registry,
+ tabbed_browser_stubs, info):
+ """Verify closing a tab by deleting it from the completion widget."""
+ tabbed_browser_stubs[0].widget.tabs = [
+ fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
+ fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
+ fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
+ ]
+ tabbed_browser_stubs[1].widget.tabs = [
+ fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
+ ]
+ model = miscmodels.tab_focus(info=info)
+ model.set_pattern('')
+ qtmodeltester.check(model)
+
+ parent = model.index(0, 0)
+ idx = model.index(1, 0, parent)
+
+ # sanity checks
+ assert model.data(parent) == "Tabs"
+ assert model.data(idx) == '2'
+
+ model.delete_cur_item(idx)
+ actual = [tab.url() for tab in tabbed_browser_stubs[0].widget.tabs]
+ assert actual == [QUrl('https://github.com'),
+ QUrl('https://duckduckgo.com')]
+
+
def test_tab_completion_not_sorted(qtmodeltester, fake_web_tab, win_registry,
tabbed_browser_stubs):
"""Ensure that the completion row order is the same as tab index order.
diff --git a/tests/unit/components/test_hostblock.py b/tests/unit/components/test_hostblock.py
index 00a7a5f8f..5949f92f8 100644
--- a/tests/unit/components/test_hostblock.py
+++ b/tests/unit/components/test_hostblock.py
@@ -427,7 +427,7 @@ def test_invalid_utf8(config_stub, tmp_path, caplog, host_blocker_factory, locat
with caplog.at_level(logging.ERROR):
current_download.successful = True
current_download.finished.emit()
- expected = r"Failed to decode: " r"b'https://www.example.org/\xa0localhost"
+ expected = r"Failed to decode: b'https://www.example.org/\xa0localhost"
assert caplog.messages[-2].startswith(expected)
else:
current_download.successful = True
@@ -565,9 +565,13 @@ def test_adblock_benchmark(data_tmpdir, benchmark, host_blocker_factory):
benchmark(lambda: blocker._is_blocked(url))
-def test_subdomain_blocking(config_stub, host_blocker_factory):
+@pytest.mark.parametrize("block_subdomains", [True, False])
+def test_subdomain_blocking(config_stub, host_blocker_factory, block_subdomains):
config_stub.val.content.blocking.method = "hosts"
config_stub.val.content.blocking.hosts.lists = None
+ config_stub.val.content.blocking.hosts.block_subdomains = block_subdomains
host_blocker = host_blocker_factory()
host_blocker._blocked_hosts.add("example.com")
- assert host_blocker._is_blocked(QUrl("https://subdomain.example.com"))
+ is_blocked = host_blocker._is_blocked(QUrl("https://subdomain.example.com"))
+ # block_subdomains is also expected result of is_blocked
+ assert is_blocked == block_subdomains
diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py
index 0375503e8..6dd09cee3 100644
--- a/tests/unit/config/test_configcommands.py
+++ b/tests/unit/config/test_configcommands.py
@@ -27,7 +27,7 @@ from PyQt5.QtCore import QUrl
from qutebrowser.config import configcommands
from qutebrowser.api import cmdutils
-from qutebrowser.utils import usertypes, urlmatch
+from qutebrowser.utils import usertypes, urlmatch, utils
from qutebrowser.keyinput import keyutils
from qutebrowser.misc import objects
@@ -529,7 +529,7 @@ class TestSource:
pyfile = config_tmpdir / 'sourced.py'
arg = 'sourced.py'
else:
- assert False, location
+ raise utils.Unreachable(location)
pyfile.write_text('\n'.join(['config.load_autoconfig(False)',
'c.content.javascript.enabled = False']),
@@ -725,15 +725,19 @@ class TestBind:
"""Get a dict with no bindings."""
return {'normal': {}}
+ @pytest.mark.parametrize("mode, url", [
+ ("normal", QUrl("qute://bindings")),
+ ("passthrough", QUrl("qute://bindings#passthrough")),
+ ])
def test_bind_no_args(self, commands, config_stub, no_bindings,
- tabbed_browser_stubs):
+ tabbed_browser_stubs, mode, url):
"""Run ':bind'.
Should open qute://bindings."""
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
- commands.bind(win_id=0)
- assert tabbed_browser_stubs[0].loaded_url == QUrl('qute://bindings')
+ commands.bind(win_id=0, mode=mode)
+ assert tabbed_browser_stubs[0].loaded_url == url
@pytest.mark.parametrize('command', ['nop', 'nope'])
def test_bind(self, commands, config_stub, no_bindings, key_config_stub,
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
index 65952ddb4..760992e15 100644
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -873,7 +873,7 @@ class TestConfigPyModules:
confpy.read()
expected = {'normal': {',a': 'message-info foo'}}
assert config.instance.get_obj('bindings.commands') == expected
- assert "qbmodule" not in sys.modules.keys()
+ assert "qbmodule" not in sys.modules
assert tmp_path not in sys.path
def test_restore_sys_on_err(self, confpy, qbmodulepy, tmp_path):
@@ -884,7 +884,7 @@ class TestConfigPyModules:
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError)
- assert "qbmodule" not in sys.modules.keys()
+ assert "qbmodule" not in sys.modules
assert tmp_path not in sys.path
def test_fail_on_nonexistent_module(self, confpy, qbmodulepy, tmp_path):
@@ -1481,7 +1481,6 @@ class TestConfigPyWriter:
def init_patch(qapp, fake_save_manager, config_tmpdir, data_tmpdir,
config_stub, monkeypatch):
monkeypatch.setattr(configfiles, 'state', None)
- yield
def test_init(init_patch, config_tmpdir):
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index 66b152937..ef3007f71 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -1876,7 +1876,6 @@ class TestShellCommand:
@pytest.mark.parametrize('kwargs, val', [
({'placeholder': True}, '[foo, bar]'),
({'placeholder': True}, '[foo, "{", "}", bar'),
- ({'placeholder': True}, '[foo, bar]'),
({'placeholder': True}, '[foo, "{fi", "le}", bar'),
# Like valid ones but with wrong placeholder
diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py
index e7ce15aff..3fc5cd561 100644
--- a/tests/unit/config/test_configutils.py
+++ b/tests/unit/config/test_configutils.py
@@ -318,13 +318,11 @@ class TestFontFamilies:
@pytest.mark.parametrize('families, quote, expected', [
(['family'], True, 'family'),
(['family1', 'family2'], True, 'family1, family2'),
- (['family'], True, 'family'),
(['space family', 'alien'], True, '"space family", alien'),
(['comma,family', 'period'], True, '"comma,family", period'),
(['family'], False, 'family'),
(['family1', 'family2'], False, 'family1, family2'),
- (['family'], False, 'family'),
(['space family', 'alien'], False, 'space family, alien'),
(['comma,family', 'period'], False, 'comma,family, period'),
])
@@ -367,9 +365,13 @@ class TestFontFamilies:
# Check the requested font to make sure CSS parsing worked
assert label.font().family() == families.family
+ # Skipping the rest of the test as WORKAROUND for
+ # https://bugreports.qt.io/browse/QTBUG-94090
+ return
+
# Try to find out whether the monospace font did a fallback on a non-monospace
# font...
- fallback_label = QLabel()
+ fallback_label = QLabel() # pylint: disable=unreachable
qtbot.add_widget(label)
fallback_label.setText("fallback")
@@ -382,9 +384,5 @@ class TestFontFamilies:
if info.family() == fallback_family:
return
- if info.family() == 'Noto Sans Mono':
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-94090
- return
-
# If we didn't fall back, we should've gotten a fixed-pitch font.
assert info.fixedPitch(), info.family()
diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py
index 4c31c5b07..076ff6e3c 100644
--- a/tests/unit/config/test_qtargs.py
+++ b/tests/unit/config/test_qtargs.py
@@ -249,7 +249,7 @@ class TestWebEngineArgs:
('single-process', True),
])
def test_process_model(self, config_stub, parser, process_model, added):
- config_stub.val.qt.process_model = process_model
+ config_stub.val.qt.chromium.process_model = process_model
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
@@ -267,7 +267,7 @@ class TestWebEngineArgs:
('never', '--disable-low-end-device-mode'),
])
def test_low_end_device_mode(self, config_stub, parser, low_end_device_mode, arg):
- config_stub.val.qt.low_end_device_mode = low_end_device_mode
+ config_stub.val.qt.chromium.low_end_device_mode = low_end_device_mode
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
@@ -277,6 +277,28 @@ class TestWebEngineArgs:
else:
assert arg in args
+ @pytest.mark.parametrize('sandboxing, arg', [
+ ('enable-all', None),
+ ('disable-seccomp-bpf', '--disable-seccomp-filter-sandbox'),
+ ('disable-all', '--no-sandbox'),
+ ])
+ def test_sandboxing(self, config_stub, parser, sandboxing, arg):
+ config_stub.val.qt.chromium.sandboxing = sandboxing
+ parsed = parser.parse_args([])
+ args = qtargs.qt_args(parsed)
+
+ remaining_flags = {
+ '--no-sandbox',
+ '--disable-seccomp-filter-sandbox',
+ }
+ if arg is not None:
+ remaining_flags.remove(arg)
+
+ if arg is not None:
+ assert arg in args
+
+ assert not set(args) & remaining_flags
+
@pytest.mark.parametrize('qt_version, referer, arg', [
# 'always' -> no arguments
('5.15.0', 'always', None),
@@ -324,7 +346,6 @@ class TestWebEngineArgs:
("light", "5.15.0", False),
("light", "5.15.1", False),
("light", "5.15.2", False),
- ("light", "5.15.2", False),
("light", "5.15.3", False),
("light", "6.0.0", False),
@@ -333,7 +354,6 @@ class TestWebEngineArgs:
("auto", "5.15.0", False),
("auto", "5.15.1", False),
("auto", "5.15.2", False),
- ("auto", "5.15.2", False),
("auto", "5.15.3", False),
("auto", "6.0.0", False),
])
diff --git a/tests/unit/config/test_websettings.py b/tests/unit/config/test_websettings.py
index 7601c7c24..f42563727 100644
--- a/tests/unit/config/test_websettings.py
+++ b/tests/unit/config/test_websettings.py
@@ -24,7 +24,7 @@ from qutebrowser.misc import objects
from qutebrowser.utils import usertypes
-@pytest.mark.parametrize([
+@pytest.mark.parametrize([ # noqa: PT006
'user_agent', 'os_info', 'webkit_version',
'upstream_browser_key', 'upstream_browser_version', 'qt_key'
], [
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/mainwindow/statusbar/test_textbase.py b/tests/unit/mainwindow/statusbar/test_textbase.py
index 631c6ce44..33c4ffd76 100644
--- a/tests/unit/mainwindow/statusbar/test_textbase.py
+++ b/tests/unit/mainwindow/statusbar/test_textbase.py
@@ -79,11 +79,11 @@ def test_text_elide_none(mocker, qtbot):
label = TextBase()
qtbot.add_widget(label)
label.setText('')
- mocker.patch('qutebrowser.mainwindow.statusbar.textbase.TextBase.'
- 'fontMetrics')
+ mock = mocker.patch(
+ 'qutebrowser.mainwindow.statusbar.textbase.TextBase.fontMetrics')
label._update_elided_text(20)
- assert not label.fontMetrics.called
+ assert not mock.called
def test_unset_text(qtbot):
diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py
index 8e5597a0e..91bdce26b 100644
--- a/tests/unit/misc/test_editor.py
+++ b/tests/unit/misc/test_editor.py
@@ -19,6 +19,7 @@
"""Tests for qutebrowser.misc.editor."""
+import sys
import time
import pathlib
import os
@@ -55,31 +56,33 @@ class TestArg:
def test_placeholder(self, config_stub, editor):
"""Test starting editor with placeholder argument."""
- config_stub.val.editor.command = ['bin', 'foo', '{}', 'bar']
+ config_stub.val.editor.command = [sys.executable, 'foo', '{}', 'bar']
editor.edit("")
editor._proc._proc.start.assert_called_with(
- "bin", ["foo", editor._filename, "bar"])
+ sys.executable, ["foo", editor._filename, "bar"])
def test_placeholder_inline(self, config_stub, editor):
"""Test starting editor with placeholder arg inside of another arg."""
- config_stub.val.editor.command = ['bin', 'foo{}', 'bar']
+ config_stub.val.editor.command = [sys.executable, 'foo{}', 'bar']
editor.edit("")
editor._proc._proc.start.assert_called_with(
- "bin", ["foo" + editor._filename, "bar"])
+ sys.executable, ["foo" + editor._filename, "bar"])
class TestFileHandling:
"""Test creation/deletion of tempfile."""
- def test_ok(self, editor):
+ @pytest.mark.parametrize('remove_file', [True, False])
+ def test_ok(self, editor, remove_file, config_stub):
"""Test file handling when closing with an exit status == 0."""
+ config_stub.val.editor.remove_file = remove_file
editor.edit("")
filename = pathlib.Path(editor._filename)
assert filename.exists()
assert filename.name.startswith('qutebrowser-editor-')
editor._proc._proc.finished.emit(0, QProcess.NormalExit)
- assert not filename.exists()
+ assert filename.exists() != config_stub.val.editor.remove_file
@pytest.mark.parametrize('touch', [True, False])
def test_with_filename(self, editor, tmp_path, touch):
diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py
index be86bf215..aaff5154e 100644
--- a/tests/unit/misc/test_guiprocess.py
+++ b/tests/unit/misc/test_guiprocess.py
@@ -26,12 +26,12 @@ 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
-@pytest.fixture()
+@pytest.fixture
def proc(qtbot, caplog):
"""A fixture providing a GUIProcess and cleaning it up after the test."""
p = guiprocess.GUIProcess('testprocess')
@@ -46,7 +46,7 @@ def proc(qtbot, caplog):
p._proc.waitForFinished()
-@pytest.fixture()
+@pytest.fixture
def fake_proc(monkeypatch, stubs):
"""A fixture providing a GUIProcess with a mocked QProcess."""
p = guiprocess.GUIProcess('testprocess')
@@ -319,8 +319,8 @@ def test_start_env(monkeypatch, qtbot, py_proc):
def test_start_detached(fake_proc):
"""Test starting a detached process."""
- cmd = 'foo'
- args = ['bar']
+ cmd = sys.executable
+ args = ['--version']
fake_proc._proc.startDetached.return_value = (True, 0)
fake_proc.start_detached(cmd, args)
fake_proc._proc.startDetached.assert_called_with(cmd, args, None)
@@ -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/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py
index 8d9c53c93..922f39331 100644
--- a/tests/unit/misc/test_keyhints.py
+++ b/tests/unit/misc/test_keyhints.py
@@ -30,8 +30,8 @@ def expected_text(*args):
"""Helper to format text we expect the KeyHintView to generate.
Args:
- args: One tuple for each row in the expected output.
- Tuples are of the form: (prefix, color, suffix, command).
+ *args: One tuple for each row in the expected output.
+ Tuples are of the form: (prefix, color, suffix, command).
"""
text = '<table>'
for group in args:
diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py
index 6e1919ec3..3c5c1eef1 100644
--- a/tests/unit/misc/test_miscwidgets.py
+++ b/tests/unit/misc/test_miscwidgets.py
@@ -38,7 +38,7 @@ class TestCommandLineEdit:
cmd_edit.set_prompt(':')
qtbot.add_widget(cmd_edit)
assert cmd_edit.text() == ''
- yield cmd_edit
+ return cmd_edit
def test_position(self, qtbot, cmd_edit):
"""Test cursor position based on the prompt."""
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_log.py b/tests/unit/utils/test_log.py
index 7682c1156..bbc6b02db 100644
--- a/tests/unit/utils/test_log.py
+++ b/tests/unit/utils/test_log.py
@@ -27,7 +27,7 @@ import warnings
import dataclasses
import pytest
-import _pytest.logging
+import _pytest.logging # pylint: disable=import-private-name
from PyQt5 import QtCore
from qutebrowser import qutebrowser
@@ -53,8 +53,7 @@ def restore_loggers():
saved_level_to_name = logging._levelToName.copy()
logger_states = {}
for name in saved_loggers:
- logger_states[name] = getattr(saved_loggers[name], 'disabled',
- None)
+ logger_states[name] = getattr(saved_loggers[name], 'disabled', None)
finally:
logging._releaseLock()
@@ -86,9 +85,9 @@ def restore_loggers():
logger_dict = logging.getLogger().manager.loggerDict
logger_dict.clear()
logger_dict.update(saved_loggers)
- for name in logger_states:
- if logger_states[name] is not None:
- saved_loggers[name].disabled = logger_states[name]
+ for name, state in logger_states.items():
+ if state is not None:
+ saved_loggers[name].disabled = state
finally:
logging._releaseLock()
@@ -349,7 +348,7 @@ class TestHideQtWarning:
"""Tests for hide_qt_warning/QtWarningFilter."""
- @pytest.fixture()
+ @pytest.fixture
def qt_logger(self):
return logging.getLogger('qt-tests')
diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py
index 2d98feece..ad9adf032 100644
--- a/tests/unit/utils/test_qtutils.py
+++ b/tests/unit/utils/test_qtutils.py
@@ -51,9 +51,7 @@ else:
test_file = None
-# pylint: disable=bad-continuation
-@pytest.mark.parametrize(['qversion', 'compiled', 'pyqt', 'version', 'exact',
- 'expected'], [
+@pytest.mark.parametrize('qversion, compiled, pyqt, version, exact, expected', [
# equal versions
('5.14.0', None, None, '5.14.0', False, True),
('5.14.0', None, None, '5.14.0', True, True), # exact=True
@@ -75,7 +73,6 @@ else:
# dev suffix
('5.15.1', '5.15.1', '5.15.2.dev2009281246', '5.15.0', False, True),
])
-# pylint: enable=bad-continuation
def test_version_check(monkeypatch, qversion, compiled, pyqt, version, exact,
expected):
"""Test for version_check().
@@ -559,7 +556,8 @@ if test_file is not None:
qiodev.mode = mode
# Create empty TESTFN file because the Python tests try to unlink
# it.after the test.
- open(test_file.TESTFN, 'w', encoding='utf-8').close()
+ with open(test_file.TESTFN, 'w', encoding='utf-8'):
+ pass
return qiodev
class PyAutoFileTests(PyIODeviceTestMixin, test_file.AutoFileTests,
@@ -742,7 +740,7 @@ class TestPyQIODevice:
with pytest.raises(io.UnsupportedOperation):
pyqiodev.seek(0, whence)
- @pytest.mark.flaky()
+ @pytest.mark.flaky
def test_qprocess(self, py_proc):
"""Test PyQIODevice with a QProcess which is non-sequential.
diff --git a/tests/unit/utils/test_resources.py b/tests/unit/utils/test_resources.py
index 738fadd37..6a11ff588 100644
--- a/tests/unit/utils/test_resources.py
+++ b/tests/unit/utils/test_resources.py
@@ -65,7 +65,7 @@ class TestReadFile:
'qutebrowser/html/unrelatedhtml',
]
- yield zipfile.Path(zip_path) / 'qutebrowser'
+ return zipfile.Path(zip_path) / 'qutebrowser'
@pytest.fixture(params=['pathlib', 'zipfile'])
def resource_root(self, request):
diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py
index e05b5d31f..f5d8204a4 100644
--- a/tests/unit/utils/test_standarddir.py
+++ b/tests/unit/utils/test_standarddir.py
@@ -31,7 +31,7 @@ import subprocess
from PyQt5.QtCore import QStandardPaths
import pytest
-from qutebrowser.utils import standarddir, utils, qtutils
+from qutebrowser.utils import standarddir, utils, qtutils, version
# Use a different application name for tests to make sure we don't change real
@@ -399,12 +399,18 @@ class TestSystemData:
"""Test system data path."""
@pytest.mark.linux
- def test_system_datadir_exist_linux(self, monkeypatch, tmpdir):
+ @pytest.mark.parametrize("is_flatpak, expected", [
+ (True, "/app/share/qute_test"),
+ (False, "/usr/share/qute_test"),
+ ])
+ def test_system_datadir_exist_linux(self, monkeypatch, tmpdir,
+ is_flatpak, expected):
"""Test that /usr/share/qute_test is used if path exists."""
monkeypatch.setenv('XDG_DATA_HOME', str(tmpdir))
monkeypatch.setattr(os.path, 'exists', lambda path: True)
+ monkeypatch.setattr(version, 'is_flatpak', lambda: is_flatpak)
standarddir._init_data(args=None)
- assert standarddir.data(system=True) == "/usr/share/qute_test"
+ assert standarddir.data(system=True) == expected
@pytest.mark.linux
def test_system_datadir_not_exist_linux(self, monkeypatch, tmpdir,
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_urlutils.py b/tests/unit/utils/test_urlutils.py
index 97ff268ca..e5773e25e 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -334,7 +334,6 @@ def test_get_search_url_open_base_url(config_stub, url, host):
Args:
url: The "URL" to enter.
host: The expected search machine host.
- query: The expected search query.
"""
config_stub.val.url.open_base_url = True
url = urlutils._get_search_url(url)
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
index 57adc883c..330ef3b96 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -809,8 +809,11 @@ class TestYaml:
assert utils.yaml_load("[1, 2]") == [1, 2]
def test_load_float_bug(self):
- with pytest.raises(yaml.YAMLError):
+ try:
utils.yaml_load("._")
+ except yaml.YAMLError:
+ # Either no exception or YAMLError, not ValueError
+ pass
def test_load_file(self, tmp_path):
tmpfile = tmp_path / 'foo.yml'
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 6c57cb3d3..7b616d8b7 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."""
@@ -718,7 +721,7 @@ class TestModuleVersions:
Args:
attribute: The name of the version attribute.
- expected: The expected return value.
+ expected_modules: The expected modules with that attribute.
"""
import_fake.version_attribute = attribute
diff --git a/tox.ini b/tox.ini
index 4be5b8620..370adbc9e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,9 +13,8 @@ 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
+passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND FORCE_COLOR DBUS_SESSION_BUS_ADDRESS
basepython =
py: {env:PYTHON:python3}
py3: {env:PYTHON:python3}
@@ -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
@@ -80,7 +78,7 @@ commands =
{[testenv:vulture]commands}
[testenv:pylint]
-basepython = {env:PYTHON:python3.8}
+basepython = {env:PYTHON:python3}
ignore_errors = true
passenv =
deps =
@@ -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