summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bumpversion.cfg2
-rw-r--r--.coveragerc1
-rw-r--r--.flake86
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/ci.yml76
-rw-r--r--.github/workflows/docker.yml62
-rw-r--r--.github/workflows/recompile-requirements.yml10
-rw-r--r--.gitignore11
-rw-r--r--.mypy.ini9
-rw-r--r--.pylintrc2
-rw-r--r--.travis.yml16
-rw-r--r--.yamllint1
-rw-r--r--README.asciidoc19
-rw-r--r--doc/changelog.asciidoc265
-rw-r--r--doc/contributing.asciidoc14
-rw-r--r--doc/faq.asciidoc33
-rw-r--r--doc/help/commands.asciidoc22
-rw-r--r--doc/help/configuring.asciidoc46
-rw-r--r--doc/help/settings.asciidoc93
-rw-r--r--doc/install.asciidoc129
-rw-r--r--doc/qutebrowser.1.asciidoc3
-rw-r--r--doc/stacktrace.asciidoc24
-rw-r--r--doc/userscripts.asciidoc2
-rw-r--r--misc/Makefile2
-rw-r--r--misc/apparmor/usr.bin.qutebrowser90
-rwxr-xr-xmisc/nsis/qutebrowser.nsi1
-rw-r--r--misc/org.qutebrowser.qutebrowser.appdata.xml1
-rw-r--r--misc/org.qutebrowser.qutebrowser.desktop2
-rw-r--r--misc/requirements/requirements-check-manifest.txt9
-rw-r--r--misc/requirements/requirements-dev.txt27
-rw-r--r--misc/requirements/requirements-dev.txt-raw1
-rw-r--r--misc/requirements/requirements-flake8.txt10
-rw-r--r--misc/requirements/requirements-mypy.txt13
-rw-r--r--misc/requirements/requirements-mypy.txt-raw3
-rw-r--r--misc/requirements/requirements-pyinstaller.txt4
-rw-r--r--misc/requirements/requirements-pylint.txt10
-rw-r--r--misc/requirements/requirements-pyqt-5.10.txt4
-rw-r--r--misc/requirements/requirements-pyqt-5.10.txt-raw4
-rw-r--r--misc/requirements/requirements-pyqt-5.11.txt4
-rw-r--r--misc/requirements/requirements-pyqt-5.11.txt-raw4
-rw-r--r--misc/requirements/requirements-pyqt-5.15.0.txt5
-rw-r--r--misc/requirements/requirements-pyqt-5.15.0.txt-raw4
-rw-r--r--misc/requirements/requirements-pyqt-5.15.txt4
-rw-r--r--misc/requirements/requirements-pyqt-5.7.txt4
-rw-r--r--misc/requirements/requirements-pyqt-5.7.txt-raw4
-rw-r--r--misc/requirements/requirements-pyqt-5.9.txt4
-rw-r--r--misc/requirements/requirements-pyqt-5.9.txt-raw4
-rw-r--r--misc/requirements/requirements-pyqt.txt4
-rw-r--r--misc/requirements/requirements-pyroma.txt2
-rw-r--r--misc/requirements/requirements-qutebrowser.txt-raw1
-rw-r--r--misc/requirements/requirements-sphinx.txt17
-rw-r--r--misc/requirements/requirements-tests-git.txt1
-rw-r--r--misc/requirements/requirements-tests.txt60
-rw-r--r--misc/requirements/requirements-tests.txt-raw16
-rw-r--r--misc/requirements/requirements-tox.txt10
-rw-r--r--misc/requirements/requirements-tox.txt-raw2
-rw-r--r--misc/requirements/requirements-vulture.txt2
-rw-r--r--misc/requirements/requirements-yamllint.txt4
-rw-r--r--misc/userscripts/README.md13
-rwxr-xr-xmisc/userscripts/cast2
-rwxr-xr-xmisc/userscripts/dmenu_qutebrowser5
-rwxr-xr-xmisc/userscripts/format_json2
-rwxr-xr-xmisc/userscripts/kodi111
-rwxr-xr-xmisc/userscripts/qr8
-rwxr-xr-xmisc/userscripts/qute-bitwarden2
-rwxr-xr-xmisc/userscripts/qute-pass54
-rwxr-xr-xmisc/userscripts/qutedmenu11
-rwxr-xr-xmisc/userscripts/readability-js56
-rw-r--r--pytest.ini7
-rw-r--r--qutebrowser/__init__.py2
-rw-r--r--qutebrowser/api/cmdutils.py17
-rw-r--r--qutebrowser/api/config.py6
-rw-r--r--qutebrowser/api/downloads.py4
-rw-r--r--qutebrowser/api/hook.py8
-rw-r--r--qutebrowser/app.py32
-rw-r--r--qutebrowser/browser/browsertab.py184
-rw-r--r--qutebrowser/browser/commands.py43
-rw-r--r--qutebrowser/browser/downloads.py35
-rw-r--r--qutebrowser/browser/downloadview.py53
-rw-r--r--qutebrowser/browser/eventfilter.py47
-rw-r--r--qutebrowser/browser/greasemonkey.py79
-rw-r--r--qutebrowser/browser/hints.py127
-rw-r--r--qutebrowser/browser/history.py8
-rw-r--r--qutebrowser/browser/inspector.py25
-rw-r--r--qutebrowser/browser/navigate.py21
-rw-r--r--qutebrowser/browser/network/pac.py7
-rw-r--r--qutebrowser/browser/pdfjs.py18
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py19
-rw-r--r--qutebrowser/browser/qutescheme.py57
-rw-r--r--qutebrowser/browser/shared.py28
-rw-r--r--qutebrowser/browser/signalfilter.py2
-rw-r--r--qutebrowser/browser/urlmarks.py5
-rw-r--r--qutebrowser/browser/webelem.py29
-rw-r--r--qutebrowser/browser/webengine/cookies.py24
-rw-r--r--qutebrowser/browser/webengine/darkmode.py305
-rw-r--r--qutebrowser/browser/webengine/interceptor.py14
-rw-r--r--qutebrowser/browser/webengine/spell.py34
-rw-r--r--qutebrowser/browser/webengine/webenginedownloads.py32
-rw-r--r--qutebrowser/browser/webengine/webengineelem.py32
-rw-r--r--qutebrowser/browser/webengine/webengineinspector.py50
-rw-r--r--qutebrowser/browser/webengine/webenginequtescheme.py41
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py135
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py327
-rw-r--r--qutebrowser/browser/webengine/webview.py84
-rw-r--r--qutebrowser/browser/webkit/cache.py9
-rw-r--r--qutebrowser/browser/webkit/cookies.py4
-rw-r--r--qutebrowser/browser/webkit/mhtml.py65
-rw-r--r--qutebrowser/browser/webkit/network/networkmanager.py31
-rw-r--r--qutebrowser/browser/webkit/tabhistory.py4
-rw-r--r--qutebrowser/browser/webkit/webkitelem.py42
-rw-r--r--qutebrowser/browser/webkit/webkitsettings.py6
-rw-r--r--qutebrowser/browser/webkit/webkittab.py26
-rw-r--r--qutebrowser/browser/webkit/webpage.py14
-rw-r--r--qutebrowser/browser/webkit/webview.py9
-rw-r--r--qutebrowser/commands/command.py36
-rw-r--r--qutebrowser/commands/runners.py16
-rw-r--r--qutebrowser/commands/userscripts.py13
-rw-r--r--qutebrowser/completion/completiondelegate.py1
-rw-r--r--qutebrowser/completion/completionwidget.py16
-rw-r--r--qutebrowser/completion/models/completionmodel.py15
-rw-r--r--qutebrowser/completion/models/histcategory.py6
-rw-r--r--qutebrowser/completion/models/listcategory.py11
-rw-r--r--qutebrowser/completion/models/miscmodels.py14
-rw-r--r--qutebrowser/completion/models/urlmodel.py11
-rw-r--r--qutebrowser/completion/models/util.py4
-rw-r--r--qutebrowser/components/adblock.py20
-rw-r--r--qutebrowser/components/misccommands.py21
-rw-r--r--qutebrowser/components/readlinecommands.py8
-rw-r--r--qutebrowser/config/config.py48
-rw-r--r--qutebrowser/config/configcache.py6
-rw-r--r--qutebrowser/config/configcommands.py28
-rw-r--r--qutebrowser/config/configdata.py59
-rw-r--r--qutebrowser/config/configdata.yml130
-rw-r--r--qutebrowser/config/configdiff.py761
-rw-r--r--qutebrowser/config/configexc.py17
-rw-r--r--qutebrowser/config/configfiles.py87
-rw-r--r--qutebrowser/config/configtypes.py428
-rw-r--r--qutebrowser/config/configutils.py107
-rw-r--r--qutebrowser/config/qtargs.py163
-rw-r--r--qutebrowser/config/stylesheet.py6
-rw-r--r--qutebrowser/config/websettings.py100
-rw-r--r--qutebrowser/extensions/interceptors.py14
-rw-r--r--qutebrowser/extensions/loader.py34
-rw-r--r--qutebrowser/html/warning-old-qt.html32
-rw-r--r--qutebrowser/html/warning-sessions.html4
-rw-r--r--qutebrowser/javascript/.eslintrc.yaml8
-rw-r--r--qutebrowser/javascript/caret.js11
-rw-r--r--qutebrowser/javascript/object_fromentries_quirk.user.js46
-rw-r--r--qutebrowser/javascript/print.js30
-rw-r--r--qutebrowser/javascript/webelem.js21
-rw-r--r--qutebrowser/keyinput/basekeyparser.py18
-rw-r--r--qutebrowser/keyinput/eventfilter.py4
-rw-r--r--qutebrowser/keyinput/keyutils.py258
-rw-r--r--qutebrowser/keyinput/macros.py14
-rw-r--r--qutebrowser/keyinput/modeman.py27
-rw-r--r--qutebrowser/keyinput/modeparsers.py16
-rw-r--r--qutebrowser/mainwindow/mainwindow.py77
-rw-r--r--qutebrowser/mainwindow/messageview.py4
-rw-r--r--qutebrowser/mainwindow/prompt.py19
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py23
-rw-r--r--qutebrowser/mainwindow/statusbar/command.py10
-rw-r--r--qutebrowser/mainwindow/statusbar/text.py82
-rw-r--r--qutebrowser/mainwindow/statusbar/url.py16
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py98
-rw-r--r--qutebrowser/mainwindow/tabwidget.py37
-rw-r--r--qutebrowser/mainwindow/windowundo.py8
-rw-r--r--qutebrowser/misc/autoupdate.py2
-rw-r--r--qutebrowser/misc/backendproblem.py113
-rw-r--r--qutebrowser/misc/checkpyver.py6
-rw-r--r--qutebrowser/misc/cmdhistory.py8
-rw-r--r--qutebrowser/misc/consolewidget.py4
-rw-r--r--qutebrowser/misc/crashdialog.py11
-rw-r--r--qutebrowser/misc/crashsignal.py18
-rw-r--r--qutebrowser/misc/debugcachestats.py8
-rw-r--r--qutebrowser/misc/earlyinit.py47
-rw-r--r--qutebrowser/misc/guiprocess.py6
-rw-r--r--qutebrowser/misc/httpclient.py15
-rw-r--r--qutebrowser/misc/ipc.py31
-rw-r--r--qutebrowser/misc/lineparser.py4
-rw-r--r--qutebrowser/misc/miscwidgets.py55
-rw-r--r--qutebrowser/misc/objects.py14
-rw-r--r--qutebrowser/misc/quitter.py16
-rw-r--r--qutebrowser/misc/savemanager.py5
-rw-r--r--qutebrowser/misc/sessions.py35
-rw-r--r--qutebrowser/misc/sql.py10
-rw-r--r--qutebrowser/misc/throttle.py14
-rw-r--r--qutebrowser/misc/utilcmds.py4
-rw-r--r--qutebrowser/qt.py7
-rw-r--r--qutebrowser/qutebrowser.py14
-rw-r--r--qutebrowser/utils/debug.py92
-rw-r--r--qutebrowser/utils/docutils.py26
-rw-r--r--qutebrowser/utils/javascript.py6
-rw-r--r--qutebrowser/utils/jinja.py16
-rw-r--r--qutebrowser/utils/log.py63
-rw-r--r--qutebrowser/utils/message.py61
-rw-r--r--qutebrowser/utils/objreg.py70
-rw-r--r--qutebrowser/utils/qtutils.py148
-rw-r--r--qutebrowser/utils/standarddir.py121
-rw-r--r--qutebrowser/utils/urlmatch.py20
-rw-r--r--qutebrowser/utils/urlutils.py104
-rw-r--r--qutebrowser/utils/usertypes.py201
-rw-r--r--qutebrowser/utils/utils.py186
-rw-r--r--qutebrowser/utils/version.py116
-rw-r--r--requirements.txt7
-rwxr-xr-xscripts/asciidoc2html.py29
-rwxr-xr-xscripts/dev/build_release.py37
-rw-r--r--scripts/dev/check_coverage.py16
-rw-r--r--scripts/dev/ci/docker/Dockerfile.j227
-rw-r--r--scripts/dev/ci/docker/README.md9
-rw-r--r--scripts/dev/ci/docker/generate.py45
-rw-r--r--scripts/dev/ci/problemmatchers.py14
-rw-r--r--scripts/dev/misc_checks.py244
-rw-r--r--scripts/dev/recompile_requirements.py192
-rwxr-xr-xscripts/dev/run_vulture.py7
-rw-r--r--scripts/dev/ua_fetch.py70
-rw-r--r--scripts/dev/update_version.py9
-rwxr-xr-xscripts/dictcli.py33
-rw-r--r--scripts/mkvenv.py232
-rwxr-xr-xscripts/open_url_in_instance.sh2
-rwxr-xr-xsetup.py3
-rw-r--r--tests/conftest.py22
-rw-r--r--tests/end2end/conftest.py5
-rw-r--r--tests/end2end/features/conftest.py1
-rw-r--r--tests/end2end/features/downloads.feature38
-rw-r--r--tests/end2end/features/hints.feature18
-rw-r--r--tests/end2end/features/javascript.feature22
-rw-r--r--tests/end2end/features/keyinput.feature29
-rw-r--r--tests/end2end/features/marks.feature10
-rw-r--r--tests/end2end/features/misc.feature31
-rw-r--r--tests/end2end/features/private.feature7
-rw-r--r--tests/end2end/features/prompts.feature7
-rw-r--r--tests/end2end/features/qutescheme.feature5
-rw-r--r--tests/end2end/features/tabs.feature20
-rw-r--r--tests/end2end/features/test_downloads_bdd.py6
-rw-r--r--tests/end2end/features/test_prompts_bdd.py19
-rw-r--r--tests/end2end/features/test_qutescheme_bdd.py9
-rw-r--r--tests/end2end/features/test_search_bdd.py10
-rw-r--r--tests/end2end/features/urlmarks.feature2
-rw-r--r--tests/end2end/fixtures/quteprocess.py120
-rw-r--r--tests/end2end/fixtures/testprocess.py18
-rw-r--r--tests/end2end/fixtures/webserver.py41
-rw-r--r--tests/end2end/fixtures/webserver_sub.py45
-rw-r--r--tests/end2end/templates/headers-link.html10
-rw-r--r--tests/end2end/test_invocations.py84
-rw-r--r--tests/end2end/test_mkvenv.py28
-rw-r--r--tests/helpers/fixtures.py27
-rw-r--r--tests/helpers/messagemock.py11
-rw-r--r--tests/helpers/stubs.py8
-rw-r--r--tests/helpers/utils.py19
-rw-r--r--tests/manual/hints/hide_unmatched_rapid_hints.html2
-rw-r--r--tests/unit/api/test_cmdutils.py18
-rw-r--r--tests/unit/browser/test_caret.py4
-rw-r--r--tests/unit/browser/test_hints.py13
-rw-r--r--tests/unit/browser/test_history.py3
-rw-r--r--tests/unit/browser/test_navigate.py2
-rw-r--r--tests/unit/browser/test_pdfjs.py54
-rw-r--r--tests/unit/browser/test_qutescheme.py3
-rw-r--r--tests/unit/browser/webengine/test_darkmode.py263
-rw-r--r--tests/unit/browser/webengine/test_spell.py59
-rw-r--r--tests/unit/browser/webengine/test_webengine_cookies.py26
-rw-r--r--tests/unit/browser/webengine/test_webenginedownloads.py7
-rw-r--r--tests/unit/browser/webengine/test_webengineinterceptor.py6
-rw-r--r--tests/unit/browser/webengine/test_webenginesettings.py12
-rw-r--r--tests/unit/browser/webengine/test_webenginetab.py3
-rw-r--r--tests/unit/browser/webkit/test_cache.py7
-rw-r--r--tests/unit/browser/webkit/test_mhtml.py41
-rw-r--r--tests/unit/browser/webkit/test_tabhistory.py3
-rw-r--r--tests/unit/commands/test_argparser.py5
-rw-r--r--tests/unit/completion/test_completiondelegate.py11
-rw-r--r--tests/unit/completion/test_completionwidget.py1
-rw-r--r--tests/unit/completion/test_models.py57
-rw-r--r--tests/unit/config/test_configcommands.py31
-rw-r--r--tests/unit/config/test_configdata.py2
-rw-r--r--tests/unit/config/test_configfiles.py42
-rw-r--r--tests/unit/config/test_configinit.py15
-rw-r--r--tests/unit/config/test_configtypes.py85
-rw-r--r--tests/unit/config/test_configutils.py43
-rw-r--r--tests/unit/config/test_qtargs.py239
-rw-r--r--tests/unit/config/test_websettings.py4
-rw-r--r--tests/unit/javascript/stylesheet/test_appendchild.js14
-rw-r--r--tests/unit/javascript/test_greasemonkey.py32
-rw-r--r--tests/unit/keyinput/test_basekeyparser.py12
-rw-r--r--tests/unit/keyinput/test_keyutils.py33
-rw-r--r--tests/unit/mainwindow/statusbar/test_url.py6
-rw-r--r--tests/unit/mainwindow/test_tabwidget.py5
-rw-r--r--tests/unit/misc/test_checkpyver.py11
-rw-r--r--tests/unit/misc/test_earlyinit.py17
-rw-r--r--tests/unit/misc/test_editor.py11
-rw-r--r--tests/unit/misc/test_ipc.py2
-rw-r--r--tests/unit/misc/test_sql.py16
-rw-r--r--tests/unit/utils/test_jinja.py2
-rw-r--r--tests/unit/utils/test_log.py19
-rw-r--r--tests/unit/utils/test_qtutils.py129
-rw-r--r--tests/unit/utils/test_standarddir.py152
-rw-r--r--tests/unit/utils/test_urlmatch.py11
-rw-r--r--tests/unit/utils/test_urlutils.py55
-rw-r--r--tests/unit/utils/test_utils.py133
-rw-r--r--tests/unit/utils/test_version.py97
-rw-r--r--tox.ini45
299 files changed, 5512 insertions, 6323 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 2628059be..fa5e2a66e 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 1.13.1
+current_version = 1.14.0
commit = True
message = Release v{new_version}
tag = True
diff --git a/.coveragerc b/.coveragerc
index 9d43917a3..cb0619b80 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -18,6 +18,7 @@ exclude_lines =
raise utils\.Unreachable
if __name__ == ["']__main__["']:
if typing.TYPE_CHECKING:
+ if TYPE_CHECKING:
\.\.\.
[xml]
diff --git a/.flake8 b/.flake8
index e913647f9..573d0856f 100644
--- a/.flake8
+++ b/.flake8
@@ -38,6 +38,7 @@ exclude = .*,__pycache__,resources.py
# D413: Missing blank line after last section (not in pep257?)
# A003: Builtin name for class attribute (needed for overridden methods)
# W504: line break after binary operator
+# FI15: __future__ import "generator_stop" missing
ignore =
B001,B008,B305,
E128,E226,E265,E501,E402,E266,E722,E731,
@@ -46,8 +47,9 @@ ignore =
P101,P102,P103,
D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D412,D413,
A003,
- W504
-min-version = 3.4.0
+ W504,
+ FI15
+min-version = 3.6.0
max-complexity = 12
per-file-ignores =
qutebrowser/api/hook.py : N801
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 123014908..000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "daily"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 36423aab8..5978f1f97 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,7 +13,7 @@ jobs:
linters:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
timeout-minutes: 10
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
@@ -39,10 +39,10 @@ jobs:
.tox
~/.cache/pip
key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- - uses: actions/setup-python@v2.1.2
+ - uses: actions/setup-python@v2
with:
python-version: '3.8'
- - uses: actions/setup-node@v2.1.1
+ - uses: actions/setup-node@v2-beta
with:
node-version: '12.x'
if: "matrix.testenv == 'eslint'"
@@ -57,7 +57,7 @@ jobs:
bindir="$HOME/.local/bin"
mkdir -p "$bindir"
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/$scversion/shellcheck-$scversion.linux.x86_64.tar.xz" | tar -xJv --strip-components 1 -C "$bindir" shellcheck-$scversion/shellcheck
- echo "::add-path::$bindir"
+ echo "$bindir" >> "$GITHUB_PATH"
fi
python -m pip install -U pip
python -m pip install -U -r misc/requirements/requirements-tox.txt
@@ -90,7 +90,7 @@ jobs:
- uses: actions/checkout@v2
- name: Set up problem matchers
run: "python scripts/dev/ci/problemmatchers.py py38 ${{ runner.temp }}"
- - run: tox -e py38
+ - run: tox -e py
tests:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -100,50 +100,38 @@ jobs:
fail-fast: false
matrix:
include:
- ### PyQt 5.7.1 (Python 3.5)
- # - testenv: py35-pyqt57
- # os: ubuntu-16.04
- # python: 3.5
- # experimental: true
- ### PyQt 5.9 (Python 3.6)
- - testenv: py36-pyqt59
- os: ubuntu-18.04
- python: 3.6
- ### PyQt 5.10 (Python 3.6)
- - testenv: py36-pyqt510
+ ### PyQt 5.12 (Python 3.6)
+ - testenv: py36-pyqt512
os: ubuntu-20.04
python: 3.6
- ### PyQt 5.11 (Python 3.7)
- - testenv: py37-pyqt511
+ ### PyQt 5.13 (Python 3.7)
+ - testenv: py37-pyqt513
os: ubuntu-20.04
python: 3.7
- ### PyQt 5.12 (Python 3.8)
- - testenv: py38-pyqt512
- os: ubuntu-20.04
- python: 3.8
- ### PyQt 5.13 (Python 3.8)
- - testenv: py38-pyqt513
- os: ubuntu-20.04
- python: 3.8
### PyQt 5.14 (Python 3.8)
- testenv: py38-pyqt514
os: ubuntu-20.04
python: 3.8
- ### PyQt 5.15 (Python 3.9)
- - testenv: py39-pyqt515
+ ### PyQt 5.15.0 (Python 3.9)
+ - testenv: py39-pyqt5150
os: ubuntu-20.04
- python: 3.9-dev
- ### PyQt 5.15 (Python 3.8, with coverage)
- - testenv: py38-pyqt515-cov
+ python: 3.9
+ ### PyQt 5.15 (Python 3.9, with coverage)
+ - testenv: py39-pyqt515-cov
os: ubuntu-20.04
- python: 3.8
- ### macOS: PyQt 5.14 (Python 3.7)
- - testenv: py37-pyqt514
+ python: 3.9
+ ### macOS: PyQt 5.15 (Python 3.7 to match PyInstaller env)
+ - testenv: py37-pyqt515
os: macos-10.15
python: 3.7
args: "tests/unit" # Only run unit tests on macOS
- ### Windows: PyQt 5.14 (Python 3.7)
- - testenv: py37-pyqt514
+ ### macOS Big Sur
+ - testenv: py37-pyqt515
+ os: macos-11.0
+ python: 3.7
+ args: "tests/unit" # Only run unit tests on macOS
+ ### Windows: PyQt 5.15 (Python 3.7 to match PyInstaller env)
+ - testenv: py37-pyqt515
os: windows-2019
python: 3.7
runs-on: "${{ matrix.os }}"
@@ -157,7 +145,7 @@ jobs:
~/.cache/pip
key: "${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- name: Set up Python
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2
with:
python-version: "${{ matrix.python }}"
- name: Set up problem matchers
@@ -178,14 +166,14 @@ jobs:
if: "failure()"
- name: Upload coverage
if: "endsWith(matrix.testenv, '-cov')"
- uses: codecov/codecov-action@v1.0.13
+ uses: codecov/codecov-action@v1
with:
name: "${{ matrix.testenv }}"
codeql:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
timeout-minutes: 30
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
steps:
- name: Checkout repository
uses: actions/checkout@v2
@@ -207,12 +195,12 @@ jobs:
irc:
timeout-minutes: 2
continue-on-error: true
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
needs: [linters, tests, tests-docker, codeql]
if: "always() && github.repository_owner == 'qutebrowser'"
steps:
- name: Send success IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.linters.result == 'success' && needs.tests.result == 'success' && needs.tests-docker.result == 'success' && needs.codeql.result == 'success'"
with:
server: chat.freenode.net
@@ -220,7 +208,7 @@ jobs:
nickname: qutebrowser-bot
message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})"
- name: Send failure IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.linters.result == 'failure' || needs.tests.result == 'failure' || needs.tests-docker.result == 'failure' || needs.codeql.result == 'failure'"
with:
server: chat.freenode.net
@@ -229,7 +217,7 @@ jobs:
message: "[${{ github.workflow }}] \u00034FAIL:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})\n
linters: ${{ needs.linters.result }}, tests: ${{ needs.tests.result }}, tests-docker: ${{ needs.tests-docker.result }}, codeql: ${{ needs.codeql.result }}"
- name: Send skipped IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.linters.result == 'skipped' || needs.tests.result == 'skipped' || needs.tests-docker.result == 'skipped' || needs.codeql.result == 'skipped'"
with:
server: chat.freenode.net
@@ -237,7 +225,7 @@ jobs:
nickname: qutebrowser-bot
message: "[${{ github.workflow }}] \u00038Skipped:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})"
- name: Send cancelled IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.linters.result == 'cancelled' || needs.tests.result == 'cancelled' || needs.tests-docker.result == 'cancelled' || needs.codeql.result == 'cancelled'"
with:
server: chat.freenode.net
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 000000000..06707eb3f
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,62 @@
+name: Rebuild Docker CI images
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: "23 5 * * *" # daily at 5:23
+
+jobs:
+ docker:
+ if: "github.repository == 'qutebrowser/qutebrowser'"
+ runs-on: ubuntu-20.04
+ strategy:
+ matrix:
+ image:
+ - archlinux-webkit
+ - archlinux-webengine
+ - archlinux-webengine-unstable
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: '3.x'
+ - run: pip install jinja2
+ - name: Generate Dockerfile
+ run: python3 generate.py ${{ matrix.image }}
+ working-directory: scripts/dev/ci/docker/
+ - uses: docker/setup-buildx-action@v1
+ - uses: docker/login-action@v1
+ with:
+ username: qutebrowser
+ password: ${{ secrets.DOCKER_TOKEN }}
+ - uses: docker/build-push-action@v2
+ with:
+ file: scripts/dev/ci/docker/Dockerfile
+ context: .
+ tags: "qutebrowser/ci:${{ matrix.image }}"
+ push: ${{ github.ref == 'refs/heads/master' }}
+
+ irc:
+ timeout-minutes: 2
+ continue-on-error: true
+ runs-on: ubuntu-20.04
+ needs: [docker]
+ if: "always() && github.repository == 'qutebrowser/qutebrowser'"
+ steps:
+ - name: Send success IRC notification
+ uses: Gottox/irc-message-action@v1.1
+ if: "needs.docker.result == 'success'"
+ with:
+ server: chat.freenode.net
+ channel: '#qutebrowser-dev'
+ nickname: qutebrowser-bot
+ message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})"
+ - name: Send non-success IRC notification
+ uses: Gottox/irc-message-action@v1.1
+ if: "needs.docker.result != 'success'"
+ with:
+ server: chat.freenode.net
+ channel: '#qutebrowser-dev'
+ nickname: qutebrowser-bot
+ message: "[${{ github.workflow }}] \u00034FAIL:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})\n
+ linters: ${{ needs.linters.result }}, tests: ${{ needs.tests.result }}, tests-docker: ${{ needs.tests-docker.result }}, codeql: ${{ needs.codeql.result }}"
diff --git a/.github/workflows/recompile-requirements.yml b/.github/workflows/recompile-requirements.yml
index 045f2ee1e..c939aa81d 100644
--- a/.github/workflows/recompile-requirements.yml
+++ b/.github/workflows/recompile-requirements.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2
with:
python-version: '3.7'
- name: Set up Python 3.8
- uses: actions/setup-python@v2.1.2
+ uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Recompile requirements
run: "python3 scripts/dev/recompile_requirements.py ${{ github.events.input.environments }}"
id: requirements
- name: Create pull request
- uses: peter-evans/create-pull-request@v2
+ uses: peter-evans/create-pull-request@v3
with:
committer: qutebrowser bot <bot@qutebrowser.org>
author: qutebrowser bot <bot@qutebrowser.org>
@@ -60,7 +60,7 @@ jobs:
if: "always() && github.repository == 'qutebrowser/qutebrowser'"
steps:
- name: Send success IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.update.result == 'success'"
with:
server: chat.freenode.net
@@ -68,7 +68,7 @@ jobs:
nickname: qutebrowser-bot
message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})"
- name: Send non-success IRC notification
- uses: Gottox/irc-message-action@v1
+ uses: Gottox/irc-message-action@v1.1
if: "needs.update.result != 'success'"
with:
server: chat.freenode.net
diff --git a/.gitignore b/.gitignore
index aa5b853f7..31c4ca3b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,15 @@
__pycache__
*.py~
*.pyc
-*.swp
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+[._]*.un~
+Session.vim
+Sessionx.vim
+*~
/build
/dist
/qutebrowser.egg-info
@@ -27,6 +35,7 @@ __pycache__
/.pytest_cache
/.testmondata
/.hypothesis
+/.benchmarks
.mypy_cache
/prof
/venv
diff --git a/.mypy.ini b/.mypy.ini
index 1972e5040..d629f012c 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -1,6 +1,4 @@
[mypy]
-# We also need to support 3.5, but if we'd chose that here, we'd need to deal
-# with conditional imports (like secrets.py).
python_version = 3.6
# --strict
@@ -38,10 +36,6 @@ ignore_missing_imports = True
# https://bitbucket.org/birkenfeld/pygments-main/issues/1485/type-hints
ignore_missing_imports = True
-[mypy-cssutils]
-# Pretty much inactive currently
-ignore_missing_imports = True
-
[mypy-pypeg2]
# Pretty much inactive currently
ignore_missing_imports = True
@@ -115,6 +109,9 @@ disallow_untyped_defs = True
[mypy-qutebrowser.browser.webengine.webengineelem]
disallow_untyped_defs = True
+[mypy-qutebrowser.browser.webengine.darkmode]
+disallow_untyped_defs = True
+
[mypy-qutebrowser.keyinput.*]
disallow_untyped_defs = True
diff --git a/.pylintrc b/.pylintrc
index 2d7cbc430..eb77aa2d5 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -74,7 +74,7 @@ valid-metaclass-classmethod-first-arg=cls
[TYPECHECK]
ignored-modules=PyQt5,PyQt5.QtWebKit
-ignored-classes=DummyBox
+ignored-classes=DummyBox,__cause__
[IMPORTS]
known-third-party=sip
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9a56a756c..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-dist: xenial
-language: python
-python: 3.5
-os: linux
-env: TESTENV=py35-pyqt57
-
-install:
- - python -m pip install -U pip
- - python -m pip install -U -r misc/requirements/requirements-tox.txt
- - ulimit -c unlimited
-
-script:
- - tox -e "$TESTENV"
-
-after_failure:
- - bash scripts/dev/ci/backtrace.sh
diff --git a/.yamllint b/.yamllint
index 638c16210..8e4d4a388 100644
--- a/.yamllint
+++ b/.yamllint
@@ -9,6 +9,7 @@ ignore: |
rules:
document-start: disable
line-length:
+ max: 88
ignore: |
/.github/*.yml
/.github/workflows/*.yml
diff --git a/README.asciidoc b/README.asciidoc
index 6f4bf2a4a..5437036be 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -109,10 +109,9 @@ Requirements
The following software and libraries are required to run qutebrowser:
-* https://www.python.org/[Python] 3.5.2 or newer (3.6 - 3.8 recommended;
- support for 3.5 will be dropped with qutebrowser v2.0.0)
-* https://www.qt.io/[Qt] 5.7.1 or newer (5.14 recommended; support for < 5.11
- will be dropped with qutebrowser v2.0.0) with the following modules:
+* https://www.python.org/[Python] 3.6 or newer
+* https://www.qt.io/[Qt] 5.12.0 or newer (5.12 LTS or 5.15 recommended)
+ with the following modules:
- QtCore / qtbase
- QtQuick (part of qtbase in some distributions)
- QtSQL (part of qtbase in some distributions)
@@ -124,8 +123,8 @@ The following software and libraries are required to run qutebrowser:
revision with known unpatched vulnerabilities. Please use it carefully and
avoid visiting untrusted websites and using it for transmission of
sensitive data.**
-* https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
- (5.14 recommended, support for < 5.11 will be dropped soon) for Python 3
+* https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.12.0 or newer
+ for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* https://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]
@@ -135,8 +134,6 @@ The following software and libraries are required to run qutebrowser:
The following libraries are optional:
-* http://cthedot.de/cssutils/[cssutils] (for an improved `:download --mhtml`
- with QtWebKit).
* On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log
output.
* http://asciidoc.org/[asciidoc] to generate the documentation for the `:help`
@@ -218,11 +215,11 @@ Active
* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
-* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/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 http://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly)
* https://vieb.dev/[Vieb] (JavaScript, Electron)
* Chrome/Chromium addons:
https://vimium.github.io/[Vimium],
+ https://github.com/dcchambers/vb4c[vb4c] (fork of cVim)
* Firefox addons (based on WebExtensions):
https://github.com/tridactyl/tridactyl[Tridactyl],
https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental),
@@ -230,7 +227,6 @@ Active
https://github.com/amedama41/vvimpulation[VVimpulation]
* Addons for Firefox and Chrome:
https://github.com/brookhong/Surfingkeys[Surfingkeys],
- https://github.com/lusakasa/saka-key[Saka Key],
https://krabby.netlify.com/[Krabby],
https://lydell.github.io/LinkHints/[Link Hints] (hinting only)
* Addons for Safari:
@@ -252,6 +248,7 @@ original site is gone but the Arch Linux wiki has some data)
* 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://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* Firefox addons (not based on WebExtensions or no recent activity):
http://www.vimperator.org/[Vimperator],
http://bug.5digits.org/pentadactyl/index[Pentadactyl],
@@ -261,7 +258,7 @@ original site is gone but the Arch Linux wiki has some data)
* 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[Saka Key] (https://github.com/lusakasa/saka-key/issues/171[unmaintained]),
https://github.com/1995eaton/chromium-vim[cVim],
https://glee.github.io/[GleeBox]
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 17635fd50..0508cec72 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -15,15 +15,219 @@ 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.
-v1.14.0 (unreleased)
+v2.0.0 (unreleased)
+-------------------
+
+Major changes
+~~~~~~~~~~~~~
+
+- At least Python 3.6 is now required to run qutebrowser, support for Python
+ 3.5 is dropped. Note that Python 3.5 is
+ https://www.python.org/downloads/release/python-3510/[no longer supported
+ upstream] since September 2020.
+- At least Qt/PyQt 5.12 is now required to run qutebrowser, support for 5.7 to
+ 5.11 (inclusive) is dropped. While Debian Buster ships Qt 5.11, it's based on a
+ Chromium version from 2018 with
+ https://www.debian.org/releases/buster/amd64/release-notes/ch-information.en.html#browser-security[no Debian security support]
+ and unsupported upstream since May 2019.
+ It also has compatibility issues with various websites (GitHub, Twitch, Android
+ Developer documentation, YouTube, ...). Since no newer Debian Stable is released
+ at the time of writing, it's recommended to
+ https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc#installing-qutebrowser-with-virtualenv[install qutebrowser in a virtualenv]
+ with a newer version of Qt/PyQt.
+- Windows 7 is not supported anymore by the Windows binaries.
+- The (formerly optional) `cssutils` dependency is now removed. It was only
+ needed for improved behavior in corner cases when using `:download --mhtml`
+ with the (non-default) QtWebKit backend, and as such it's unlikely anyone is
+ still relying on it. The `cssutils` project is also dead upstream, with its
+ repository being gone after Bitbucket
+ https://bitbucket.org/blog/sunsetting-mercurial-support-in-bitbucket[removed Mercurial support].
+
+Removed
+~~~~~~~
+
+- The `--enable-webengine-inspector` flag (which was only needed for Qt 5.10 and
+ below) is now dropped. With Qt 5.11 and newer, the inspector/devtools are
+ enabled unconditionally.
+- Support for moving qutebrowser data from versions before v1.0.0 has been
+ removed.
+- The `--old` flag for `:config-diff` has been removed. It used to show
+ customized options for the old pre-v1.0 config files (in order to aid
+ migration to v1.0).
+- The `:inspector` command which was deprecated in v1.13.0 (in favor of
+ `:devtools`) is now removed.
+
+Added
+~~~~~
+
+- When QtWebEngine has been updated but PyQtWebEngine hasn't yet, the dark mode
+ settings might stop working. As a (currently undocumented) escape hatch, this
+ version adds a `QUTE_DARKMODE_VARIANT=qt_515_2` environment variable which can
+ be set to get the correct behavior in (transitive) situations like this.
+- New userscripts:
+ - `kodi` to play videos in Kodi
+ - `qr` to generate a QR code of the current URL
+
+Changed
+~~~~~~~
+
+- `config.py` files now are required to have either
+ `config.load_autoconfig(False)` (don't load `autoconfig.yml`) or
+ `config.load_autoconfig()` (do load `autoconfig.yml`) in them.
+- (TODO) Windows and macOS releases now ship Python 3.9 rather than 3.7.
+- The `colors.webpage.darkmode.*` settings are now also supported with older Qt
+ versions (Qt 5.12 and 5.13) rather than just with Qt 5.14 and above.
+- For regexes in the config (`hints.{prev,next}_regexes`), certain patterns
+ which will change meanings in future Python versions are now disallowed. This is
+ the case for character sets starting with a literal `[` or containing literal
+ character sequences `--`, `&&`, `~~`, or `||`. To avoid a warning, remove the
+ duplicate characters or escape them with a backslash.
+- If `prompt(..., "default")` is used via JS, the default text is now
+ pre-selected in the prompt shown by qutebrowser.
+- URLs such as `::1/foo` are now handled as a search term or local file rather
+ than IPv6. Use `[::1]/foo` to force parsing as IPv6 instead.
+- The `mkvenv.py` script now runs a "smoke test" after setting up the virtual
+ environment to ensure it's working as expected. If necessary, the test can be
+ skipped via a new `--skip-smoke-test` flag.
+- Both qutebrowser userscripts and Greasemonkey scripts are now additionally
+ picked up from qutebrowser's config directory (the `userscripts` and
+ `greasemonkey` subdirectories of e.g. `~/.config/qutebrowser/`) rather than only
+ the data directory (the same subdirectories of e.g.
+ `~/.local/share/qutebrowser/`).
+
+Fixed
+~~~~~
+
+- With interpolated color settings (`colors.tabs.indicator.*` and
+ `colors.downloads.*`), the alpha channel is now handled correctly.
+- The `format_json` userscript now uses `env` in its shebang, making it work
+ correctly on systems where `bash` isn't located in `/bin`.
+
+v1.14.1 (unreleased)
+--------------------
+
+Added
+~~~~~
+
+- With v1.14.0, qutebrowser configures the main window to be transparent, so
+ that it's possible to configure a translucent tab- or statusbar. However, that
+ change introduced various issues, such as performance degradation on some
+ systems or breaking dmenu window embedding with its `-w` option. To avoid those
+ issues for people who are not using transparency, the default behavior is
+ reverted to versions before v1.14.0 in this release. A new `window.transparent`
+ setting can be set to `true` to restore the behavior of v1.14.0.
+
+Changed
+~~~~~~~
+
+- Windows and macOS releases now ship Qt 5.15.2, which is based on
+ Chromium 83.0.4103.122 with security fixes up to 86.0.4240.183. This includes
+ CVE-2020-15999 in the bundled freetype library, which is known to be exploited
+ in the wild. It also includes various other bugfixes/features compared to
+ Qt 5.15.0 included in qutebrowser v1.14.0, such as:
+ * Correct handling of AltGr on Windows
+ * Fix for `content.cookies.accept` not working properly
+ * Fixes for screen sharing (some websites are still broken until an upcoming Qt
+ 5.15.3)
+ * Support for FIDO U2F / WebAuth
+ * Fix for the unwanted creation of directories such as `databases-incognito` in
+ the home directory
+ * Proper autocompletion in the devtools console
+ * Proper signalisation of a tab's audible status (`[A]`)
+ * Fix for a hang when opening the context menu on macOS Big Sur (11.0)
+ * Hardware accelerated graphics on macOS
+
+Fixed
+~~~~~
+
+- Setting the `content.headers.referer` setting to `same-domain` (the default)
+ was supposed to truncate referers to only the host with QtWebEngine.
+ Unfortunately, this functionality broke in Qt 5.14. It works properly again
+ with this release, including a test so this won't happen again.
+- With QtWebEngine 5.15, setting the `content.headers.referer` setting to
+ `never` did still send referers. This is now fixed as well.
+- In v1.14.0, a regression was introduced, causing a crash when qutebrowser was
+ closed after opening a download with PDF.js. This is now fixed.
+- With Qt 5.12, the `Object.fromEntries` JavaScript API is unavailable (it was
+ introduced in Chromium 73, while Qt 5.12 is based on 69). This caused
+ https://www.vr.fi/en and possibly other websites to break when accessed with Qt
+ 5.12. A suitable polyfill is now included with qutebrowser if
+ `content.site_specific_quirks` is enabled (which is the default).
+- While XDG startup notifications (e.g. launch feedback via the bouncy cursor
+ in KDE Plasma) were supported ever since Qt 5.1, qutebrowser's desktop file
+ accidentally declared that it wasn't supported. This is now fixed.
+- The `dmenu_qutebrowser` and `qutedmenu` userscripts now correctly read the
+ qutebrowser sqlite history which has been in use since v1.0.0.
+- With Python 3.8+ and vertical tabs, a deprecation warning for an implicit int
+ conversion was shown. This is now fixed.
+- Ever since Qt 5.11, fetching more completion data when that data is loaded
+ lazily (such as with history) and the last visible item is selected was broken.
+ The exact reason is currently unknown, but this release adds a tenative fix.
+- When PgUp/PgDown were used to go beyond the last visible item, the above issue
+ caused a crash, which is now also fixed.
+- As a workaround for an overzealous Microsoft Defender false-positive detecting
+ a "trojan" in the (unprocessed) adblock list, `:adblock-update` now doesn't
+ cache the HTTP response anymore.
+- With the QtWebKit backend and `content.headers` set to `same-domain` (the
+ default), origins with the same domain but different schemes or ports were
+ treated as the same domain. They now are correctly treated as different domains.
+- When a URL path uses percent escapes (such as
+ `https://example.com/embedded%2Fpath`), using `:navigate up` would treat the
+ `%2F` as a path separator and replace any remaining percent escapes by their
+ unescaped equivalents. Those are now handled correctly.
+- On macOS 11.0 (Big Sur), the default monospace font name caused a parsing error, thus
+ resulting in broken styling for the completion, hints, and other UI components.
+ They now look properly again.
+- Due to a Qt bug, installing Qt/PyQt from prebuilt binaries on systems with a
+ very old `libxcb-utils` version (notably, Debian Stable, but not Ubuntu since
+ 16.04 LTS) results in a setup which fails to start. This also affects the
+ `mkvenv.py` script, which now includes a workaround for this case.
+- The `open_url_instance.sh` userscript now complains when `socat` is not
+ installed, rather than silencing the error.
+- The example AppArmor profile in `misc/` was outdated and written for the
+ older QtWebKit backend. It is now updated to serve as an useful starting
+ point with QtWebEngine.
+- When running `:devtools` on Fedora without the needed (optional) dependency
+ installed, it was suggested to install `qt5-webengine-devtools`, which does
+ not, in fact, exist. It's now correctly suggested to install
+ `qt5-qtwebengine-devtools` instead.
+- With Qt 5.15.2, lines/borders coming from the `readability-js` userscript
+ were invisible. This is now fixed by changing the border color to grey (with all
+ Qt versions).
+- Minor performance improvements.
+- Due to changes in the underlying Chromium, the
+ `colors.webpage.prefers_color_scheme_dark` setting broke with Qt 5.15.2. It now
+ works properly again.
+- (TODO) Fix for various functionality breaking in private windows with v1.14.0,
+ after the last private window is closed. This includes:
+ * Ad blocking
+ * Downloads
+ * Site-specific quirks (e.g. for Google login)
+ * Certain settings such as `content.javascript.enabled`
+
+v1.14.0 (2020-10-15)
--------------------
+Note: The QtWebEngine version bundled with the Windows/macOS
+releases is still based on Qt 5.15.0 (like with qutebrowser v1.12.0 and
+v1.13.0) rather than Qt 5.15.1 because of a
+https://bugreports.qt.io/browse/QTBUG-86752[Qt bug] causing
+frequent renderer process crashes. When Qt 5.15.2 is released
+(planned for November 3rd, 2020), a qutebrowser v1.14.x patch
+release with an updated QtWebEngine will be released.
+
+Furthermore, this release still only contains partial session support for QtWebEngine
+5.15. It's still recommended to run against Qt 5.15 due to the security patches
+contained in it -- for most users, the added workarounds seem to work out fine. A
+rewritten session support will be part of qutebrowser v2.0.0, tentatively planned for the
+end of the year or early 2021.
+
Changed
~~~~~~~
- The `content.media_capture` setting got split up into three more fine-grained
settings, `content.media.audio_capture`, `.video_capture` and
- `.audio_video_capture`. Before this change, anwering "always" to a prompt
+ `.audio_video_capture`. Before this change, answering "always" to a prompt
about e.g. audio capturing would set the `content.media_capture` setting,
which would also allow the same website to capture video on a future visit.
Now every prompt will set the appropriate setting, though existing
@@ -36,8 +240,7 @@ Changed
partially transparent qutebrowser window on a setup which supports doing so.
- If QtWebEngine is compiled with PipeWire support and libpipewire is
installed, qutebrowser will now support screen sharing on Wayland. Note that
- QtWebEngine 5.15.1 (planned for August 2020) is needed, though the Archlinux
- qt5-webengine package backports the patch.
+ QtWebEngine 5.15.1 is needed.
- When `:undo` is used with a count, it now reopens the count-th to last tab
instead of the last one. The depth can instead be passed as an argument,
which is also completed.
@@ -45,17 +248,47 @@ Changed
- `:back` and `:forward` now take an optional index which is completed using
the current tab's history.
- The time a website in a tab was visited is now saved/restored in sessions.
-- New argument `strip` for `:navigate` which removes queries and
- fragments from the current URL.
- When attempting to download a file to a location for which there's already a
still-running download, a confirmation prompt is now displayed.
- `:completion-item-focus` now understands `next-page` and `prev-page` with
corresponding `<PgDown>` / `<PgUp>` default bindings.
- When the last private window is closed, all private browsing data is now cleared.
+- When `config.source(...)` is used with a `--config-py` argument given,
+ qutebrowser used to search relative files in the config basedir, leading to them
+ not being found when using a shared `config.py` for different basedirs. Instead,
+ they are now searched relative to the given `config.py` file.
+- `navigate prev` (`[[`) and `navigate next` (`]]`) now recognize links with
+ `nav-prev` and `nav-next` classes, such as those used by the Hugo static site
+ generator.
+- When `tabs.favicons` is disabled but `tabs.tabs_are_windows` is set, the
+ window icon is still set to the page's favicon now.
+- The `--asciidoc` argument to `src2asciidoc.py` and `build_release.py` now
+ only takes the path to `asciidoc.py`, using the current Python interpreter by
+ default. To configure the Python interpreter as well, use
+ `--asciidoc-python path/to/python --asciidoc path/to/asciidoc.py`
+ instead of the former
+ `--asciidoc path/to/python path/to/asciidoc.py`.
+- Dark mode (`colors.webpage.darkmode.*`) is now supported with Qt 5.15.2 (which
+ is not released yet).
+- The default for the darkmode `policy.images` setting is now set to `smart`
+ which fixes issues with e.g. formulas on Wikipedia.
+- The `readability-js` userscript now adds some CSS to improve the reader mode
+ styling in various scenarios:
+ * Images are now shrinked to the page width, similarly to what Firefox' reader
+ mode does.
+ * Some images ore now displayed as block (rather than inline) which is what
+ Firefox' reader mode does as well.
+ * Blockquotes are now styled more distinctively, again based on the Firefox
+ reader mode.
+ * Code blocks are now easier to distinguish from text and tables have visible
+ cell margins.
+- The `readability-js` userscript now supports hint userscript mode.
Added
~~~~~
+- New argument `strip` for `:navigate` which removes queries and
+ fragments from the current URL.
- `:undo` now has a new `-w` / `--window` argument, which can be used to
restore closed windows (rather than tabs). This is bound to `U` by default.
- `:jseval` can now take `javascript:...` URLs via a new `--url` flag.
@@ -72,6 +305,11 @@ Added
open the directory containing the downloaded file. An entry to do the same
was also added to the context menu.
- Messages are now wrapped when they are too long to be displayed on a single line.
+- New possible `--debug-flag` values:
+ * `wait-renderer-process` waits for a `SIGUSR1` in the renderer process so a
+ debugger can be attached.
+ * `avoid-chromium-init` allows using `--version` without needing a working
+ QtWebEngine/Chromium.
Fixed
~~~~~
@@ -109,10 +347,21 @@ Fixed
could end up in a confusing state. This is now fixed.
- When qutebrowser quits, running downloads are now cancelled properly.
- The site-specific quirk for `web.whatsapp.com` has been updated to work after recent
- WhatsApp-changes.
+ changes in WhatsApp.
- Highlighting in the completion now works properly when UTF-16 surrogate pairs (such as
emoji) are involved.
- When a windowed inspector is clicked, insert mode now isn't entered anymore.
+- When `:undo` is used to re-open a tab, but `tabs.tabs_are_windows` was set between
+ closing and undoing the close, qutebrowser crashed. This is now fixed.
+- With QtWebEngine 5.15.0, setting the darkmode image policy to `smart` leads to
+ renderer process crashes. The offending setting value is now ignored with a
+ warning.
+- Fixes for the `qute-pass` userscript:
+ * With newer `gopass` versions, a deprecation notice was copied as
+ password due to `qute-pass` using it in a deprecated way.
+ * The `--password-store` argument didn't actually set
+ `PASSWORD_STORE_DIR` for `pass`, resulting in `qute-pass` finding matches but the
+ underlying `pass` not finding matching passwords.
v1.13.1 (2020-07-17)
--------------------
@@ -1128,7 +1377,7 @@ Fixed
- `qute://` pages now work properly on Qt 5.11.2
- Error when passing a substring with spaces to `:tab-take`.
-- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly.
+- Greasemonkey scripts which start with a UTF-8 BOM are now handled correctly.
- When no documentation has been generated, the plaintext documentation now can
be shown for more files such as `qute://help/userscripts.html`.
- Crash when doing initial run on Wayland without XWayland.
diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc
index dbf1e5cc5..3960dec27 100644
--- a/doc/contributing.asciidoc
+++ b/doc/contributing.asciidoc
@@ -9,7 +9,7 @@ IMPORTANT: Bandwidth for pull request review is currently quite limited. If you
want to contribute where it's most needed, please consider reviewing or testing
open pull requests.
-I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
+I `&lt;3` footnote:[`<3` in HTML] contributors!
This document contains guidelines for contributing to qutebrowser, as well as
useful hints when doing so.
@@ -111,9 +111,9 @@ unittests and several linters/checkers.
Currently, the following tox environments are available:
* Tests using https://www.pytest.org[pytest]:
- - `py35`, `py36`: Run pytest for python 3.5/3.6 with the system-wide PyQt.
- - `py36-pyqt57`, ..., `py36-pyqt59`: Run pytest with the given PyQt version (`py35-*` also works).
- - `py36-pyqt59-cov`: Run with coverage support (other Python/PyQt versions work too).
+ - `py36`, `py37`, ...: Run pytest for python 3.6/3.7/... with the system-wide PyQt.
+ - `py36-pyqt512`, ..., `py36-pyqt515`: Run pytest with the given PyQt version (`py35-*` also works).
+ - `py36-pyqt515-cov`: Run with coverage support (other Python/PyQt versions work too).
* `flake8`: Run various linting checks via https://pypi.python.org/pypi/flake8[flake8].
* `vulture`: Run https://pypi.python.org/pypi/vulture[vulture] to find
unused code portions.
@@ -586,9 +586,9 @@ can be useful for debugging:
- chrome://gpuclean/ (crashes the current renderer process!)
- chrome://ppapiflashcrash/
- chrome://ppapiflashhang/
-- chrome://quota-internals/ (Qt 5.11)
-- chrome://taskscheduler-internals/ (Qt 5.11)
-- chrome://sandbox/ (Qt 5.11, Linux only)
+- chrome://quota-internals/
+- chrome://taskscheduler-internals/
+- chrome://sandbox/ (Linux only)
QtWebEngine internals
~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc
index e0b102683..39df56faa 100644
--- a/doc/faq.asciidoc
+++ b/doc/faq.asciidoc
@@ -190,7 +190,6 @@ For QtWebKit:
+
For QtWebEngine:
-. Make sure your versions of PyQt and Qt are 5.8 or higher.
. Use `dictcli.py` script to install dictionaries.
Run the script with `-h` for the parameter description.
. Set `spellcheck.languages` to the desired list of languages, e.g.:
@@ -226,7 +225,7 @@ Why does it take longer to open a URL in qutebrowser than in chromium?::
One workaround is to use this
https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script]
and place it in your $PATH with the name "qutebrowser". This
- script passes the URL via an unix socket to qutebrowser (if its
+ script passes the URL via a unix socket to qutebrowser (if its
running already) using socat which is much faster and starts a new
qutebrowser if it is not running already.
@@ -253,11 +252,6 @@ Note that there are some missing features which you may run into:
. Some scripts expect `GM_xmlhttpRequest` to ignore Cross Origin Resource
Sharing restrictions, this is currently not supported, so scripts making
requests to third party sites will often fail to function correctly.
-. If your backend is a QtWebEngine version 5.8, 5.9 or 5.10 then regular
- expressions are not supported in `@include` or `@exclude` rules. If your
- script uses them you can re-write them to use glob expressions or convert
- them to `@match` rules.
- See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info.
. Any greasemonkey API function to do with adding UI elements is not currently
supported. That means context menu extentensions and background pages.
@@ -322,6 +316,31 @@ certutil -d "sql:${HOME}/.pki/nssdb" -D -n "My Fancy Certificate Nickname"
And then import the new and valid certificates using the procedure
described above.
+Is there a dark mode? How can I filter websites to be darker?::
+There is a total of four possible approaches to get dark websites:
++
+- The `colors.webpage.prefers_color_scheme_dark` setting tells websites that you prefer
+ a dark theme. However, this requires websites to ship an appropriate dark style sheet.
+ The setting requires a restart and QtWebEngine with at least Qt 5.14.
+- The `colors.webpage.darkmode.*` settings enable the dark mode of the underlying
+ Chromium. Those setting require a restart and QtWebEngine with at least Qt 5.14. It's
+ unfortunately not possible (due to limitations
+ https://bugs.chromium.org/p/chromium/issues/detail?id=952419[in Chromium] and/or
+ https://bugreports.qt.io/browse/QTBUG-84484[QtWebEngine]) to
+ change them dynamically or to specify a list of excluded websites.
+ There is some remaining hope to
+ https://github.com/qutebrowser/qutebrowser/issues/5542[allow for this]
+ using HTML/CSS features, but so far nobody has been able to get things to
+ work (even with Chromium) - help welcome!
+- The `content.user_stylesheets` setting allows specifying a custom CSS such as
+ https://github.com/alphapapa/solarized-everything-css/[Solarized Everything]. Despite
+ the name, the repository also offers themes other than just Solarized. This approach
+ often yields worse results compared to the above ones, but it's possible to toggle it
+ dynamically using a binding like `:bind ,d 'config-cycle content.user_stylesheets
+ ~/path/to/solarized-everything-css/css/gruvbox/gruvbox-all-sites.css ""'`
+- Finally, qutebrowser's Greasemonkey support should allow for running a
+ https://github.com/darkreader/darkreader/issues/926#issuecomment-575893299[stripped down version]
+ of the Dark Reader extension. This is mostly untested, though.
== Troubleshooting
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index 6c9b7f1a8..f31283e9c 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -334,14 +334,8 @@ Remove a key from a dict.
[[config-diff]]
=== config-diff
-Syntax: +:config-diff [*--old*]+
-
Show all customized options.
-==== optional arguments
-* +*-o*+, +*--old*+: Show difference for the pre-v1.0 files (qutebrowser.conf/keys.conf).
-
-
[[config-edit]]
=== config-edit
Syntax: +:config-edit [*--no-source*]+
@@ -605,7 +599,7 @@ Syntax: +:greasemonkey-reload [*--force*]+
Re-read Greasemonkey scripts from disk.
-The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data directory (see `:version`).
+The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data or config directories (see `:version`).
==== optional arguments
* +*-f*+, +*--force*+: For any scripts that have required dependencies, re-download them.
@@ -765,7 +759,16 @@ Evaluate a JavaScript string.
* +*-u*+, +*--url*+: Interpret js-code as a `javascript:...` URL.
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
-* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in.
+* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in. Predefined world names are:
+
+
+ - `main` (same world as the web page's JavaScript and
+ Greasemonkey, unless overridden via `@qute-js-world`)
+ - `application` (used for internal qutebrowser JS code,
+ should not be used via `:jseval` unless you know what
+ you're doing)
+ - `user` (currently unused)
+ - `jseval` (used for this command by default)
==== note
@@ -1307,7 +1310,8 @@ Note that the command is *not* run in a shell, so things like `$VAR` or `> outpu
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
* +*-o*+, +*--output*+: Show the output in a new tab.
* +*-m*+, +*--output-messages*+: Show the output as messages.
-* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
+* +*-d*+, +*--detach*+: Detach the command from qutebrowser so that it continues running when qutebrowser quits.
+
==== count
Given to userscripts as $QUTE_COUNT.
diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc
index 90b7ed65b..89866ccce 100644
--- a/doc/help/configuring.asciidoc
+++ b/doc/help/configuring.asciidoc
@@ -9,9 +9,7 @@ qutebrowser's config files
--------------------------
qutebrowser releases before v1.0.0 had a `qutebrowser.conf` and `keys.conf`
-file. Those are not used anymore since that release - see
-<<migrating,"Migrating older configurations">> for information on how to
-migrate to the new config.
+file. Those are not used anymore since v1.0.0.
When using `:set` and `:bind`, changes are saved to an `autoconfig.yml` file
automatically. If you don't want to have a config file which is curated by
@@ -395,9 +393,10 @@ 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]
-- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
+- 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]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
@@ -421,8 +420,8 @@ stable across qutebrowser versions):
# pylint: disable=C0111
from qutebrowser.config.configfiles import ConfigAPI # noqa: F401
from qutebrowser.config.config import ConfigContainer # noqa: F401
-config = config # type: ConfigAPI # noqa: F821 pylint: disable=E0602,C0103
-c = c # type: ConfigContainer # noqa: F821 pylint: disable=E0602,C0103
+config: ConfigAPI = config # noqa: F821 pylint: disable=E0602,C0103
+c: ConfigContainer = c # noqa: F821 pylint: disable=E0602,C0103
----
emacs-like config
@@ -438,38 +437,3 @@ Various emacs/conkeror-like keybinding configs exist:
It's also mostly possible to get rid of modal keybindings by setting
`input.insert_mode.auto_enter` to `false`, and `input.forward_unbound_keys` to
`all`.
-
-[[migrating]]
-Migrating older configurations
-------------------------------
-
-qutebrowser does no automatic migration for the new configuration. However,
-there's a special link:qute://configdiff/old[configdiff] page
-(`qute://configdiff/old`) in qutebrowser, which will show you the changes you
-did in your old configuration, compared to the old defaults.
-
-Other changes in default settings:
-
-- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history
- if no text was entered yet.
- With v1.0.x, they always navigate through command history instead of selecting
- completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion
- instead.
- You can get back the old behavior by doing:
-+
-----
-:bind -m command <Up> completion-item-focus prev
-:bind -m command <Down> completion-item-focus next
-----
-+
-or always navigate through command history with
-+
-----
-:bind -m command <Up> command-history-prev
-:bind -m command <Down> command-history-next
-----
-
-- The default for `completion.web_history.max_items` is now set to `-1`, showing
- an unlimited number of items in the completion for `:open` as the new
- sqlite-based completion is much faster. If the `:open` completion is too slow
- on your machine, set an appropriate limit again.
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index a9c959089..309f1ab1d 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -327,6 +327,7 @@
|<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`.
|<<window.hide_decoration,window.hide_decoration>>|Hide the window decoration.
|<<window.title_format,window.title_format>>|Format to use for the window title. The same placeholders like for
+|<<window.transparent,window.transparent>>|Set the main window background to transparent.
|<<zoom.default,zoom.default>>|Default zoom level.
|<<zoom.levels,zoom.levels>>|Available zoom levels.
|<<zoom.mouse_divider,zoom.mouse_divider>>|Number of zoom increments to divide the mouse wheel movements to.
@@ -1570,6 +1571,7 @@ Default: +pass:[white]+
[[colors.webpage.darkmode.algorithm]]
=== colors.webpage.darkmode.algorithm
Which algorithm to use for modifying how colors are rendered with darkmode.
+The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated like `lightness-hsl` with older QtWebEngine versions.
This setting requires a restart.
@@ -1577,15 +1579,13 @@ Type: <<types,String>>
Valid values:
- * +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value.
+ * +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value. Not available with Qt < 5.14.
* +lightness-hsl+: Modify colors by converting them to the HSL color space and inverting the lightness (i.e. the "L" in HSL).
* +brightness-rgb+: Modify colors by subtracting each of r, g, and b from their maximum value.
Default: +pass:[lightness-cielab]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.contrast]]
=== colors.webpage.darkmode.contrast
@@ -1598,9 +1598,7 @@ Type: <<types,Float>>
Default: +pass:[0.0]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.enabled]]
=== colors.webpage.darkmode.enabled
@@ -1626,9 +1624,7 @@ Type: <<types,Bool>>
Default: +pass:[false]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.grayscale.all]]
=== colors.webpage.darkmode.grayscale.all
@@ -1641,9 +1637,7 @@ Type: <<types,Bool>>
Default: +pass:[false]+
-On QtWebEngine, this setting requires Qt 5.14 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.grayscale.images]]
=== colors.webpage.darkmode.grayscale.images
@@ -1663,7 +1657,7 @@ On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.policy.images]]
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
-WARNING: On Qt 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt].
+With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt].
This setting requires a restart.
@@ -1673,13 +1667,11 @@ Valid values:
* +always+: Apply dark mode filter to all images.
* +never+: Never apply dark mode filter to any images.
- * +smart+: Apply dark mode based on image content.
+ * +smart+: Apply dark mode based on image content. Not available with Qt 5.15.0.
-Default: +pass:[never]+
-
-On QtWebEngine, this setting requires Qt 5.14 or newer.
+Default: +pass:[smart]+
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.policy.page]]
=== colors.webpage.darkmode.policy.page
@@ -1735,6 +1727,8 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.prefers_color_scheme_dark
Force `prefers-color-scheme: dark` colors for websites.
+This setting requires a restart.
+
Type: <<types,Bool>>
Default: +pass:[false]+
@@ -1900,7 +1894,6 @@ Default:
[[content.autoplay]]
=== content.autoplay
Automatically start playing `<video>` elements.
-Note: On Qt < 5.11, this option needs a restart and does not support URL patterns.
This setting supports URL patterns.
@@ -1908,9 +1901,7 @@ Type: <<types,Bool>>
Default: +pass:[true]+
-On QtWebEngine, this setting requires Qt 5.10 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[content.cache.appcache]]
=== content.cache.appcache
@@ -1979,12 +1970,9 @@ Valid values:
Default: +pass:[all]+
-On QtWebEngine, this setting requires Qt 5.11 or newer.
-
[[content.cookies.store]]
=== content.cookies.store
Store cookies.
-Note this option needs a restart with QtWebEngine on Qt < 5.9.
Type: <<types,Bool>>
@@ -2002,7 +1990,6 @@ Default: +pass:[iso-8859-1]+
[[content.desktop_capture]]
=== content.desktop_capture
Allow websites to share screen content.
-On Qt < 5.10, a dialog box is always displayed, even if this is set to "true".
This setting supports URL patterns.
@@ -2026,7 +2013,7 @@ Type: <<types,Bool>>
Default: +pass:[true]+
-On QtWebEngine, this setting requires Qt 5.12 or newer.
+This setting is only available with the QtWebEngine backend.
[[content.frame_flattening]]
=== content.frame_flattening
@@ -2400,9 +2387,7 @@ Valid values:
Default: +pass:[ask]+
-On QtWebEngine, this setting requires Qt 5.8 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[content.mute]]
=== content.mute
@@ -2469,9 +2454,7 @@ Valid values:
Default: +pass:[ask]+
-On QtWebEngine, this setting requires Qt 5.11 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[content.plugins]]
=== content.plugins
@@ -2493,7 +2476,7 @@ Type: <<types,Bool>>
Default: +pass:[true]+
-On QtWebEngine, this setting requires Qt 5.8 or newer.
+This setting is only available with the QtWebEngine backend.
[[content.private_browsing]]
=== content.private_browsing
@@ -2544,9 +2527,7 @@ Valid values:
Default: +pass:[ask]+
-On QtWebEngine, this setting requires Qt 5.11 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[content.site_specific_quirks]]
=== content.site_specific_quirks
@@ -2590,9 +2571,7 @@ Valid values:
Default: +pass:[allow-from-user-interaction]+
-On QtWebEngine, this setting requires Qt 5.11 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[content.user_stylesheets]]
=== content.user_stylesheets
@@ -2615,7 +2594,6 @@ Default: +pass:[true]+
[[content.webrtc_ip_handling_policy]]
=== content.webrtc_ip_handling_policy
Which interfaces to expose via WebRTC.
-On Qt 5.10, this option doesn't work because of a Qt bug.
This setting requires a restart.
@@ -2630,9 +2608,7 @@ Valid values:
Default: +pass:[all-interfaces]+
-On QtWebEngine, this setting requires Qt 5.9.2 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[content.xss_auditing]]
=== content.xss_auditing
@@ -3558,7 +3534,7 @@ Valid values:
* +always+: Always show the scrollbar.
* +never+: Never show the scrollbar.
* +when-searching+: Show the scrollbar when searching for text in the webpage. With the QtWebKit backend, this is equal to `never`.
- * +overlay+: Show an overlay scrollbar. With Qt < 5.11 or on macOS, this is unavailable and equal to `when-searching`; with the QtWebKit backend, this is equal to `never`. Enabling/disabling overlay scrollbars requires a restart.
+ * +overlay+: Show an overlay scrollbar. On macOS, this is unavailable and equal to `when-searching`; with the QtWebKit backend, this is equal to `never`. Enabling/disabling overlay scrollbars requires a restart.
Default: +pass:[overlay]+
@@ -3677,9 +3653,7 @@ Valid values:
Default: empty
-On QtWebEngine, this setting requires Qt 5.8 or newer.
-
-On QtWebKit, this setting is unavailable.
+This setting is only available with the QtWebEngine backend.
[[statusbar.padding]]
=== statusbar.padding
@@ -4046,14 +4020,14 @@ The following placeholders are defined:
* `{perc}`: Percentage as a string like `[10%]`.
* `{perc_raw}`: Raw percentage, e.g. `10`.
* `{current_title}`: Title of the current web page.
-* `{title_sep}`: The string ` - ` if a title is set, empty otherwise.
+* `{title_sep}`: The string `" - "` if a title is set, empty otherwise.
* `{index}`: Index of this tab.
* `{aligned_index}`: Index of this tab padded with spaces to have the same
width.
* `{id}`: Internal tab ID of this tab.
* `{scroll_pos}`: Page scroll position.
* `{host}`: Host of the current web page.
-* `{backend}`: Either ''webkit'' or ''webengine''
+* `{backend}`: Either `webkit` or `webengine`
* `{private}`: Indicates when private mode is enabled.
* `{current_url}`: URL of the current web page.
* `{protocol}`: Protocol (http/https/...) of the current web page.
@@ -4175,6 +4149,7 @@ characters in the search terms are replaced by safe characters (called
expands to `slash%2Fand%26amp`).
* `{unquoted}` quotes nothing (for `slash/and&amp` this placeholder
expands to `slash/and&amp`).
+* `{0}` means the same as `{}`, but can be used multiple times.
The search engine named `DEFAULT` is used when `url.auto_search` is turned
on and something else than a URL was entered to be opened. Other search
@@ -4232,6 +4207,22 @@ Type: <<types,FormatString>>
Default: +pass:[{perc}{current_title}{title_sep}qutebrowser]+
+[[window.transparent]]
+=== window.transparent
+Set the main window background to transparent.
+
+This allows having a transparent tab- or statusbar (might require a compositor such
+as picom). However, it breaks some functionality such as dmenu embedding via its
+`-w` option. On some systems, it was additionally reported that main window
+transparency negatively affects performance.
+
+Note this setting only affects windows opened after setting it.
+
+
+Type: <<types,Bool>>
+
+Default: +pass:[false]+
+
[[zoom.default]]
=== zoom.default
Default zoom level.
diff --git a/doc/install.asciidoc b/doc/install.asciidoc
index 0f9a4c399..f7a3d8a60 100644
--- a/doc/install.asciidoc
+++ b/doc/install.asciidoc
@@ -27,48 +27,50 @@ On Debian / Ubuntu
How to install qutebrowser depends a lot on the version of Debian/Ubuntu you're
running.
-Ubuntu 16.04 LTS / Linux Mint 18
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+[[ubuntu1604]]
+Debian Stretch / Ubuntu 16.04 LTS / Linux Mint 18
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Debian Stretch does have QtWebEngine packaged, but only in a very old and insecure
+version (Qt 5.7, based on a Chromium from March 2016). Furthermore, it packages Python
+3.5 which is not supported anymore since qutebrowser v2.0.0.
Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or
-QtWebEngine). However, it comes with Python 3.5, so you can
-<<tox,install qutebrowser in a virtualenv>>.
+QtWebEngine) and also comes with Python 3.5.
+
+You should be able to install a newer Python (3.6+) using the
+https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or
+https://github.com/pyenv/pyenv[pyenv], and then proceed to
+<<tox,install qutebrowser in a virtualenv>>. However, this is currently untested. If you
+got this setup to work successfully, please submit a pull request to adjust these
+instructions!
-You'll need some basic libraries to use the virtualenv-installed PyQt:
+Note you'll need some basic libraries to use the virtualenv-installed PyQt:
----
-# apt install libglib2.0-0 libgl1 libfontconfig1 libx11-xcb1 libxi6 libxrender1 libdbus-1-3
+# apt install --no-install-recommends git ca-certificates python3 python3-venv asciidoc libglib2.0-0 libgl1 libfontconfig1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1 libxkbcommon-x11-0 libdbus-1-3 libyaml-dev gcc python3-dev
----
-Debian Stretch
-~~~~~~~~~~~~~~
-
-Debian Stretch comes with QtWebEngine in the repositories. This makes it possible
-to install qutebrowser via the Debian package.
-
-You'll need to download three packages:
+// FIXME not needed anymore?
+// libxi6 libxrender1 libegl1-mesa
-- https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2] (a library
- used by qutebrowser which is not in the earlier repositories)
-- https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] itself
-- Either https://packages.debian.org/sid/all/qutebrowser-qtwebengine/download[qutebrowser-qtwebengine]
- or https://packages.debian.org/sid/all/qutebrowser-qtwebkit/download[qutebrowser-qtwebkit]
- (or both) depending on the backend you want to use. QtWebEngine is the
- default/recommended choice.
+Debian Buster / Ubuntu 18.04 LTS / Linux Mint 19
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-After downloading, install the packages (make sure to install all the
-downloaded qutebrowser deb files in one apt command):
+Debian Buster packages qutebrowser, but ships a very old version (v1.6.1 from March
+2019). The QtWebEngine library used for rendering web contents is also very old (Qt
+5.11, based on a Chromium from March 2018) and insecure. It is
+https://www.debian.org/releases/buster/amd64/release-notes/ch-information.en.html#browser-security[not covered]
+by Debian's security patches. It's recommended to <<tox,install qutebrowser in a
+virtualenv>> with a newer PyQt/Qt binary instead.
-----
-# apt install ./python3-pypeg2_*_all.deb
-# apt install ./qutebrowser*.deb
-----
-
-For an update after the initial install, you only need to download/install the
-qutebrowser package.
+With Ubuntu 18.04, the situation looks similar (but worse): There, qutebrowser v1.1.1
+from January 2018 is packaged, with QtWebEngine 5.9 based on a Chromium from January
+2017. It's recommended to either upgrade to Ubuntu 20.04 LTS or <<tox,install
+qutebrowser in a virtualenv>> with a newer PyQt/Qt binary instead.
-Debian Buster / Ubuntu 18.04 LTS / Linux Mint 19 (or newer)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Ubuntu 20.04 LTS / Linux Mint 20 (or newer)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
With those distributions, qutebrowser is in the official repositories, and you
can install it with apt:
@@ -80,23 +82,26 @@ can install it with apt:
Additional hints
~~~~~~~~~~~~~~~~
-- Alternatively, you can <<tox,install qutebrowser in a virtualenv>> to get a newer
- QtWebEngine version.
- If running from git, run the following to generate the documentation for the
- `:help` command:
+ `:help` command (the `mkvenv.py` script used with a virtualenv install already does
+ this for you):
+
----
-# apt install --no-install-recommends asciidoc source-highlight
+# apt install --no-install-recommends asciidoc
$ python3 scripts/asciidoc2html.py
----
-- If you prefer using QtWebKit, there's an up-to-date version available in
- https://packages.debian.org/buster/libqt5webkit5[Debian Testing].
+- If you prefer using QtWebKit, there's QtWebKit 5.212 available in
+ Ubuntu 18.04 / Debian Buster or newer. Note however that it is based on an upstream
+ WebKit from September 2016 with known security issues and no sandboxing or process
+ isolation.
- If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
+
----
# apt install gstreamer1.0-plugins-{bad,base,good,ugly}
----
++
+Note those are only needed with QtWebKit, not with the (default) QtWebEngine backend.
On Fedora
---------
@@ -141,7 +146,8 @@ $ cd ..
$ rm -r qutebrowser-git
----
-or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
+or you could use an AUR helper like https://github.com/Jguer/yay/[yay], e.g.
+`yay -S qutebrowser-git`.
If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
@@ -181,12 +187,6 @@ with:
# xbps-install qutebrowser
----
-It's currently recommended to install `python3-PyQt5-webengine` and
-`python3-PyQt5-opengl`, then start with `--backend webengine` to use the new
-backend.
-
-Since the v1.0 release, qutebrowser uses QtWebEngine by default.
-
On NixOS
--------
@@ -197,18 +197,11 @@ it with:
$ nix-env -i qutebrowser
----
-It's recommended to install `qt5.qtwebengine` and start with
-`--backend webengine` to use the new backend.
-
-Since the v1.0 release, qutebrowser uses QtWebEngine by default.
-
On openSUSE
-----------
There are prebuilt RPMs available at https://software.opensuse.org/download.html?project=network&package=qutebrowser[OBS].
-To use the QtWebEngine backend, install `libqt5-qtwebengine`.
-
On Slackware
------------
@@ -248,7 +241,7 @@ qutebrowser is available
https://flathub.org/apps/details/org.qutebrowser.qutebrowser[on Flathub]
as `org.qutebrowser.qutebrowser`.
-WARNING: As of July 2020, the Flatpak package is severely outdated (qutebrowser
+WARNING: As of October 2020, the Flatpak package is severely outdated (qutebrowser
v1.7.0 from July 2019) and, among other issues, misses fixes for a
(low-severity) https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-4rcq-jv2f-898j[security issue].
It's recommended to <<tox,install qutebrowser in a virtualenv>> instead, which
@@ -350,10 +343,14 @@ qutebrowser from source.
==== Homebrew
----
-$ brew install qt5
+$ brew install qt
+(build PyQt and PyQtWebEngine from source)
$ pip3 install qutebrowser
----
+NOTE: Homebrew does not package PyQtWebEngine (Python wrappers for
+QtWebEngine), so you will need to build that from sources manually.
+
Since the v1.0 release, qutebrowser uses QtWebEngine by default.
Homebrew's builds of Qt and PyQt don't come with QtWebKit (and `--with-qtwebkit`
@@ -364,12 +361,11 @@ https://github.com/annulen/webkit/wiki/Building-QtWebKit-on-OS-X[manually].
Packagers
---------
-There are example .desktop and icon files provided. They would go in the
-standard location for your distro (`/usr/share/applications` and
-`/usr/share/pixmaps` for example).
-
-The normal `setup.py install` doesn't install these files, so you'll have to do
-it as part of the packaging process.
+qutebrowser ships with a
+https://github.com/qutebrowser/qutebrowser/blob/master/misc/Makefile[Makefile]
+intended for packagers. This installs system-wide files in a proper locations,
+so it should be preferred to the usual `setup.py install` or `pip install`
+invocation.
// The tox anchor is so that old links remain compatible.
// When switching to Sphinx, that should be changed.
@@ -405,6 +401,10 @@ $ cd qutebrowser
Installing dependencies (including Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Using a Qt installed via virtualenv needs a couple of system-wide libraries.
+See the <<ubuntu1604,Ubuntu 16.04 section>> for details about which libraries
+are required.
+
Then run the install script:
----
@@ -418,9 +418,8 @@ This installs all needed Python dependencies in a `.venv` subfolder
This comes with an up-to-date Qt/PyQt including a pre-compiled QtWebEngine
binary, but has a few caveats:
-- Make sure your `python3` is Python 3.5 or newer, otherwise you'll get a "No
- matching distribution found" error. Note that qutebrowser itself also requires
- this.
+- Make sure your `python3` is Python 3.6 or newer, otherwise you'll get a "No
+ matching distribution found" error and/or qutebrowser will not run.
- It only works on 64-bit x86 systems, with other architectures you'll get the
same error.
- It comes with a QtWebEngine compiled without proprietary codec support (such
@@ -433,6 +432,12 @@ You can specify a Qt/PyQt version with the `--pyqt-version` flag, see
`mkenv.py --help` for a list of available versions. By default, the latest
version which plays well with qutebrowser is used.
+NOTE: If qutebrowser fails to start with a _"This application failed to start
+because no Qt platform plugin could be initialized."_ message, most likely a
+system-wide library is missing. Run qutebrowser again after
+`export QT_DEBUG_PLUGINS=1` and keep attention to a
+_QLibraryPrivate::loadPlugin failed on ..._ line for details.
+
Installing dependencies (system-wide Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc
index 1fcac0609..777eddc65 100644
--- a/doc/qutebrowser.1.asciidoc
+++ b/doc/qutebrowser.1.asciidoc
@@ -62,9 +62,6 @@ show it.
*--backend* '{webkit,webengine}'::
Which backend to use.
-*--enable-webengine-inspector*::
- Enable the web inspector / devtools for QtWebEngine. Note that this is a SECURITY RISK and you should not visit untrusted websites with the inspector turned on. See https://bugreports.qt.io/browse/QTBUG-50725 for more details. This is not needed anymore since Qt 5.11 where the inspector is always enabled and secure.
-
=== debug arguments
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
Override the configured console loglevel
diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc
index 9bda4ab87..3d95aa25e 100644
--- a/doc/stacktrace.asciidoc
+++ b/doc/stacktrace.asciidoc
@@ -28,10 +28,20 @@ Debian/Ubuntu/...
^^^^^^^^^^^^^^^^^
For Debian based systems (Debian, Ubuntu, Linux Mint, ...), debug information
-is available in the repositories:
+is for QtWebEngine is available in a dedicated repository. Enable that repository
+(https://wiki.debian.org/HowToGetABacktrace#Installing_the_debugging_symbols[Debian],
+https://wiki.ubuntu.com/Debug%20Symbol%20Packages[Ubuntu],
+https://www.linuxmint.com/rel_tessa_mate_whatsnew.php[Linux Mint]) and install
+the debug packages:
----
-# apt-get install python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg python3-dbg libqt5webkit5-dbg
+# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebengine-dbg libqt5webengine5-dbgsym libqt5webenginecore5-dbgsym
+----
+
+or with the QtWebKit backend:
+
+----
+# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg libqt5webkit5-dbg
----
Fedora
@@ -116,7 +126,7 @@ First install `gdb` on your system if it's not installed already.
Then run qutebrowser directly inside gdb like this:
----
-$ gdb $(readlink -f $(which python3)) -ex 'run -m qutebrowser --debug'
+$ gdb -ex r --args $(readlink -f $(which python3)) -m qutebrowser --debug --temp-basedir
----
Note qutebrowser/gdb will take a long time to start. After you reproduce the
@@ -131,9 +141,10 @@ Program received signal SIGSEGV, Segmentation fault.
Now enter these commands at the gdb prompt:
----
+(gdb) set pagination off
+(gdb) set logging overwrite on
(gdb) set logging on
-(gdb) bt full
-# you might have to press enter a few times until you get the prompt back
+(gdb) bt
(gdb) quit
----
@@ -176,9 +187,10 @@ Getting the stack trace
Now enter these commands at the gdb prompt:
----
+(gdb) set pagination off
+(gdb) set logging overwrite on
(gdb) set logging on
(gdb) bt
-# you might have to press enter a few times until you get the prompt back
(gdb) quit
----
diff --git a/doc/userscripts.asciidoc b/doc/userscripts.asciidoc
index 2dc34402d..9bbc68ce0 100644
--- a/doc/userscripts.asciidoc
+++ b/doc/userscripts.asciidoc
@@ -18,7 +18,7 @@ mpv, a simple key binding to something like `:spawn mpv {url}` should suffice.
Also note userscripts need to have the executable bit set (`chmod +x`) for
qutebrowser to run them.
-To call a userscript, it needs to be stored in your data directory under
+To call a userscript, it needs to be stored in your config or data directory under
`userscripts` (for example: `~/.local/share/qutebrowser/userscripts/myscript`),
or just use an absolute path.
diff --git a/misc/Makefile b/misc/Makefile
index 985541fdd..b916a20d5 100644
--- a/misc/Makefile
+++ b/misc/Makefile
@@ -29,7 +29,7 @@ install: man
install -Dm644 icons/qutebrowser.svg \
"$(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps/qutebrowser.svg"
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/userscripts/" \
- $(wildcard misc/userscripts/*)
+ $(filter-out misc/userscripts/__pycache__,$(wildcard misc/userscripts/*))
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/scripts/" \
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
diff --git a/misc/apparmor/usr.bin.qutebrowser b/misc/apparmor/usr.bin.qutebrowser
index b993e0058..3d27be697 100644
--- a/misc/apparmor/usr.bin.qutebrowser
+++ b/misc/apparmor/usr.bin.qutebrowser
@@ -1,41 +1,79 @@
-# AppArmor profile for qutebrowser
-# Tested on Debian jessie
-
#include <tunables/global>
profile qutebrowser /usr/{local/,}bin/qutebrowser {
-
#include <abstractions/base>
+ #include <abstractions/python>
+ #include <abstractions/audio>
+ #include <abstractions/dri-common>
+ #include <abstractions/mesa>
+ #include <abstractions/X>
+ #include <abstractions/wayland>
+ #include <abstractions/qt5>
+ #include <abstractions/fonts>
+
+ #include <abstractions/dbus-session-strict>
#include <abstractions/nameservice>
#include <abstractions/openssl>
#include <abstractions/ssl_certs>
- #include <abstractions/audio>
- #include <abstractions/fonts>
- #include <abstractions/kde>
+
+ #include <abstractions/freedesktop.org>
#include <abstractions/user-download>
- #include <abstractions/X>
+ #include <abstractions/user-tmp>
- capability dac_override,
- /usr/{local/,}bin/ r,
- /usr/{local/,}bin/qutebrowser rix,
- /usr/bin/python3.? r,
+ # not nice but required for chromium sandbox
+ capability sys_admin,
+ capability sys_chroot,
+ capability sys_ptrace,
- /usr/lib/python3/ mr,
- /usr/lib/python3/** mr,
- /usr/lib/python3.?/ r,
- /usr/lib/python3.?/** mr,
- /usr/local/lib/python3.?/** r,
+ /dev/ r,
+ /dev/video* r,
+ /etc/mime.types r,
+ /usr/bin/ r,
+ /usr/bin/ldconfig ix,
+ /usr/bin/uname ix,
+ /usr/bin/qutebrowser rix,
+ /usr/lib/qt/libexec/QtWebEngineProcess mrix,
+ /usr/share/pdf.js/** r,
+ /usr/share/qt/translations/qtwebengine_locales/* r,
+ /usr/share/qt/qtwebengine_dictionaries r,
+ /usr/share/qt/qtwebengine_dictionaries/* r,
+ /usr/share/qt/resources/* r,
- /proc/*/mounts r,
- owner /tmp/** rwkl,
- owner /run/user/*/ rw,
- owner /run/user/*/** krw,
+ owner @{HOME}/ r,
+ owner /dev/shm/.org.chromium* rw,
+ owner @{HOME}/.cache/{qtshadercache,qutebrowser}/** rwlk,
+ owner @{HOME}/.cache/qtshadercache** rwl,
+ owner @{HOME}/.config/qutebrowser/** rwlk,
+ owner @{HOME}/.local/share/.org.chromium.Chromium* rw,
+ owner @{HOME}/.local/share/mime/generic-icons r,
+ owner @{HOME}/.local/share/qutebrowser/ r,
+ owner @{HOME}/.local/share/qutebrowser/** rwkl,
+ owner @{HOME}/.pki/nssdb/* rwk,
+ owner @{HOME}/#[0-9]* rwm,
+ owner /run/user/*/qutebrowser/ rw,
+ owner /run/user/*/qutebrowser/* rw,
+ owner /run/user/*/qutebrowser*slave-socket rwl,
+ owner /run/user/*/#* rw,
- @{HOME}/.config/qutebrowser/** krw,
- @{HOME}/.local/share/qutebrowser/** krw,
- @{HOME}/.cache/qutebrowser/** krw,
- @{HOME}/.gstreamer-0.10/* r,
+ # qt/kde
+ @{PROC} r,
+ @{PROC}/sys/fs/inotify/max_user_watches r,
+ @{PROC}/sys/kernel/random/boot_id r,
+ @{PROC}/sys/kernel/core_pattern r,
+ @{PROC}/sys/kernel/yama/ptrace_scope r,
+ /sys/{class,bus}/ r,
+ /sys/bus/pci/devices/ r,
+ /sys/devices/**/{class,config,device,resource,revision,removable,uevent} r,
+ /sys/devices/**/{vendor,subsystem_device,subsystem_vendor} r,
-}
+ owner @{PROC}/@{pid}/{fd,stat,task,mounts}/ r,
+ owner @{PROC}/@{pid}/stat r,
+ owner @{PROC}/@{pid}/task/@{pid}/status r,
+ owner @{PROC}/@{pid}/{setgroups,gid_map,oom_score_adj,uid_map} rw,
+ owner @{PROC}/@{pid}/{oom_score_adj,uid_map} rw,
+
+ # allow execution of userscripts
+ /usr/share/qutebrowser/userscripts/* Ux,
+}
diff --git a/misc/nsis/qutebrowser.nsi b/misc/nsis/qutebrowser.nsi
index d9b8fbf8d..77fd373eb 100755
--- a/misc/nsis/qutebrowser.nsi
+++ b/misc/nsis/qutebrowser.nsi
@@ -1,4 +1,5 @@
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+# encoding: iso-8859-1
#
# This file is part of qutebrowser.
#
diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml
index e246ea4d7..f0885b8ed 100644
--- a/misc/org.qutebrowser.qutebrowser.appdata.xml
+++ b/misc/org.qutebrowser.qutebrowser.appdata.xml
@@ -44,6 +44,7 @@
</content_rating>
<releases>
<!-- Add new releases here -->
+<release version="1.14.0" date="2020-10-15"/>
<release version="1.13.1" date="2020-07-17"/>
<release version="1.13.0" date="2020-06-26"/>
<release version="1.12.0" date="2020-06-01"/>
diff --git a/misc/org.qutebrowser.qutebrowser.desktop b/misc/org.qutebrowser.qutebrowser.desktop
index a1deb319f..cf3ee0422 100644
--- a/misc/org.qutebrowser.qutebrowser.desktop
+++ b/misc/org.qutebrowser.qutebrowser.desktop
@@ -46,7 +46,7 @@ Type=Application
Categories=Network;WebBrowser;
Exec=qutebrowser %u
Terminal=false
-StartupNotify=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;
Keywords=Browser
Actions=new-window;preferences;
diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt
index 4cc00982d..e9376f0b1 100644
--- a/misc/requirements/requirements-check-manifest.txt
+++ b/misc/requirements/requirements-check-manifest.txt
@@ -1,5 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-check-manifest==0.42
-pep517==0.8.2
-toml==0.10.1
+build==0.1.0
+check-manifest==0.45
+packaging==20.7
+pep517==0.9.1
+pyparsing==2.4.7
+toml==0.10.2
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index b48786862..578ab2b64 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -1,26 +1,25 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-bump2version==1.0.0
-certifi==2020.6.20
-cffi==1.14.2
+bump2version==1.0.1
+certifi==2020.11.8
+cffi==1.14.4
chardet==3.0.4
-colorama==0.4.3
-cryptography==3.1
-cssutils==1.0.2
+colorama==0.4.4
+cryptography==3.2.1
github3.py==1.3.0
-hunter==3.2.1
+hunter==3.3.1
idna==2.10
jwcrypto==0.8
manhole==1.6.0
-packaging==20.4
+packaging==20.7
pycparser==2.20
-Pympler==0.8
+Pympler==0.9
pyparsing==2.4.7
-PyQt-builder==1.5.0
+PyQt-builder==1.6.0
python-dateutil==2.8.1
-requests==2.24.0
-sip==5.4.0
+requests==2.25.0
+sip==5.5.0
six==1.15.0
-toml==0.10.1
+toml==0.10.2
uritemplate==3.0.1
-# urllib3==1.25.10
+# urllib3==1.26.2
diff --git a/misc/requirements/requirements-dev.txt-raw b/misc/requirements/requirements-dev.txt-raw
index e7758f167..fd840bab1 100644
--- a/misc/requirements/requirements-dev.txt-raw
+++ b/misc/requirements/requirements-dev.txt-raw
@@ -1,5 +1,4 @@
hunter
-cssutils
pympler
github3.py
bump2version
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index 0764490a3..3427c0c69 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -1,12 +1,12 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==20.1.0
-flake8==3.8.3
-flake8-bugbear==20.1.4
+attrs==20.3.0
+flake8==3.8.4
+flake8-bugbear==20.11.1
flake8-builtins==1.5.3
-flake8-comprehensions==3.2.3
+flake8-comprehensions==3.3.0
flake8-copyright==0.2.2
-flake8-debugger==3.2.1
+flake8-debugger==4.0.0
flake8-deprecated==1.3
flake8-docstrings==1.5.0
flake8-future-import==0.4.6
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index 09d11ea6c..a046a0b5e 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -1,16 +1,15 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-diff-cover==3.0.1
-inflect==4.1.0
+diff-cover==4.0.1
+inflect==5.0.2
Jinja2==2.11.2
jinja2-pluralize==0.3.0
-lxml==4.5.2
+lxml==4.6.2
MarkupSafe==1.1.1
-mypy==0.782
+mypy==0.790
mypy-extensions==0.4.3
pluggy==0.13.1
-Pygments==2.6.1
--e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5_stubs
-six==1.15.0
+Pygments==2.7.2
+-e git+https://github.com/stlehmann/PyQt5-stubs.git@704207e90bee7b36ec9861dfa6b39f06a27c6718#egg=PyQt5_stubs
typed-ast==1.4.1
typing-extensions==3.7.4.3
diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw
index d3b0dc4ca..7c888ed1e 100644
--- a/misc/requirements/requirements-mypy.txt-raw
+++ b/misc/requirements/requirements-mypy.txt-raw
@@ -2,6 +2,3 @@ mypy
lxml # For HTML reports
diff-cover
-e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5-stubs
-
-# remove @commit-id for scm installs
-#@ replace: @.*# @master#
diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt
index 6de7b6fa8..b1a3e98ee 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.0
-pyinstaller-hooks-contrib==2020.7
+pyinstaller==4.1
+pyinstaller-hooks-contrib==2020.10
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index 08c1d2c10..e3856a40a 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -1,10 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==2.3.3 # rq.filter: < 2.4
-certifi==2020.6.20
-cffi==1.14.2
+certifi==2020.11.8
+cffi==1.14.4
chardet==3.0.4
-cryptography==3.1
+cryptography==3.2.1
github3.py==1.3.0
idna==2.10
isort==4.3.21
@@ -15,9 +15,9 @@ pycparser==2.20
pylint==2.4.4 # rq.filter: < 2.5
python-dateutil==2.8.1
./scripts/dev/pylint_checkers
-requests==2.24.0
+requests==2.25.0
six==1.15.0
typed-ast==1.4.1 ; python_version<"3.8"
uritemplate==3.0.1
-# urllib3==1.25.10
+# urllib3==1.26.2
wrapt==1.11.2
diff --git a/misc/requirements/requirements-pyqt-5.10.txt b/misc/requirements/requirements-pyqt-5.10.txt
deleted file mode 100644
index 69c3ccbd0..000000000
--- a/misc/requirements/requirements-pyqt-5.10.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# This file is automatically generated by scripts/dev/recompile_requirements.py
-
-PyQt5==5.10.1 # rq.filter: < 5.11
-sip==4.19.8 # rq.filter: < 5
diff --git a/misc/requirements/requirements-pyqt-5.10.txt-raw b/misc/requirements/requirements-pyqt-5.10.txt-raw
deleted file mode 100644
index 4fbea8575..000000000
--- a/misc/requirements/requirements-pyqt-5.10.txt-raw
+++ /dev/null
@@ -1,4 +0,0 @@
-#@ filter: PyQt5 < 5.11
-PyQt5 >= 5.10, < 5.11
-#@ filter: sip < 5
-sip < 5
diff --git a/misc/requirements/requirements-pyqt-5.11.txt b/misc/requirements/requirements-pyqt-5.11.txt
deleted file mode 100644
index bfee87c0f..000000000
--- a/misc/requirements/requirements-pyqt-5.11.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# This file is automatically generated by scripts/dev/recompile_requirements.py
-
-PyQt5==5.11.3 # rq.filter: < 5.12
-PyQt5-sip==4.19.19 # rq.filter: < 4.20
diff --git a/misc/requirements/requirements-pyqt-5.11.txt-raw b/misc/requirements/requirements-pyqt-5.11.txt-raw
deleted file mode 100644
index bdbe43f19..000000000
--- a/misc/requirements/requirements-pyqt-5.11.txt-raw
+++ /dev/null
@@ -1,4 +0,0 @@
-#@ filter: PyQt5 < 5.12
-PyQt5 >= 5.11, < 5.12
-
-#@ filter: PyQt5-sip < 4.20
diff --git a/misc/requirements/requirements-pyqt-5.15.0.txt b/misc/requirements/requirements-pyqt-5.15.0.txt
new file mode 100644
index 000000000..b9ee53f65
--- /dev/null
+++ b/misc/requirements/requirements-pyqt-5.15.0.txt
@@ -0,0 +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.8.1
+PyQtWebEngine==5.15.0 # rq.filter: == 5.15.0
diff --git a/misc/requirements/requirements-pyqt-5.15.0.txt-raw b/misc/requirements/requirements-pyqt-5.15.0.txt-raw
new file mode 100644
index 000000000..12d6adb7d
--- /dev/null
+++ b/misc/requirements/requirements-pyqt-5.15.0.txt-raw
@@ -0,0 +1,4 @@
+#@ filter: PyQt5 == 5.15.0
+#@ filter: PyQtWebEngine == 5.15.0
+PyQt5 == 5.15.0
+PyQtWebEngine == 5.15.0
diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt
index 93b31c40b..e791bb323 100644
--- a/misc/requirements/requirements-pyqt-5.15.txt
+++ b/misc/requirements/requirements-pyqt-5.15.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-PyQt5==5.15.0 # rq.filter: < 6
+PyQt5==5.15.2 # rq.filter: < 6
PyQt5-sip==12.8.1
-PyQtWebEngine==5.15.0 # rq.filter: < 6
+PyQtWebEngine==5.15.2 # rq.filter: < 6
diff --git a/misc/requirements/requirements-pyqt-5.7.txt b/misc/requirements/requirements-pyqt-5.7.txt
deleted file mode 100644
index 703c95a92..000000000
--- a/misc/requirements/requirements-pyqt-5.7.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# This file is automatically generated by scripts/dev/recompile_requirements.py
-
-PyQt5==5.7.1 # rq.filter: < 5.8
-sip==4.19.8 # rq.filter: < 5
diff --git a/misc/requirements/requirements-pyqt-5.7.txt-raw b/misc/requirements/requirements-pyqt-5.7.txt-raw
deleted file mode 100644
index 745deb4b9..000000000
--- a/misc/requirements/requirements-pyqt-5.7.txt-raw
+++ /dev/null
@@ -1,4 +0,0 @@
-#@ filter: PyQt5 < 5.8
-#@ filter: sip < 5
-PyQt5 >= 5.7, < 5.8
-sip < 5
diff --git a/misc/requirements/requirements-pyqt-5.9.txt b/misc/requirements/requirements-pyqt-5.9.txt
deleted file mode 100644
index 8f3258721..000000000
--- a/misc/requirements/requirements-pyqt-5.9.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# This file is automatically generated by scripts/dev/recompile_requirements.py
-
-PyQt5==5.9.2 # rq.filter: < 5.10
-sip==4.19.8 # rq.filter: < 5
diff --git a/misc/requirements/requirements-pyqt-5.9.txt-raw b/misc/requirements/requirements-pyqt-5.9.txt-raw
deleted file mode 100644
index 45d4e0c10..000000000
--- a/misc/requirements/requirements-pyqt-5.9.txt-raw
+++ /dev/null
@@ -1,4 +0,0 @@
-#@ filter: PyQt5 < 5.10
-PyQt5 >= 5.9, < 5.10
-#@ filter: sip < 5
-sip < 5
diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt
index 53a8b4af8..ec6cfd810 100644
--- a/misc/requirements/requirements-pyqt.txt
+++ b/misc/requirements/requirements-pyqt.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-PyQt5==5.15.0
+PyQt5==5.15.2
PyQt5-sip==12.8.1
-PyQtWebEngine==5.15.0
+PyQtWebEngine==5.15.2
diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt
index 6b131e155..1a2dbde7f 100644
--- a/misc/requirements/requirements-pyroma.txt
+++ b/misc/requirements/requirements-pyroma.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.16
-Pygments==2.6.1
+Pygments==2.7.2
pyroma==2.6
diff --git a/misc/requirements/requirements-qutebrowser.txt-raw b/misc/requirements/requirements-qutebrowser.txt-raw
index c66c65beb..4678b9ce5 100644
--- a/misc/requirements/requirements-qutebrowser.txt-raw
+++ b/misc/requirements/requirements-qutebrowser.txt-raw
@@ -3,5 +3,4 @@ Pygments
pyPEG2
PyYAML
colorama
-cssutils
attrs
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index da6447009..164311235 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -1,26 +1,25 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
alabaster==0.7.12
-Babel==2.8.0
-certifi==2020.6.20
+Babel==2.9.0
+certifi==2020.11.8
chardet==3.0.4
docutils==0.16
idna==2.10
imagesize==1.2.0
Jinja2==2.11.2
MarkupSafe==1.1.1
-packaging==20.4
-Pygments==2.6.1
+packaging==20.7
+Pygments==2.7.2
pyparsing==2.4.7
-pytz==2020.1
-requests==2.24.0
-six==1.15.0
+pytz==2020.4
+requests==2.25.0
snowballstemmer==2.0.0
-Sphinx==3.2.1
+Sphinx==3.3.1
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.4
-urllib3==1.25.10
+urllib3==1.26.2
diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt
index 14b6eec04..9efbeca40 100644
--- a/misc/requirements/requirements-tests-git.txt
+++ b/misc/requirements/requirements-tests-git.txt
@@ -27,7 +27,6 @@ git+https://github.com/pallets/werkzeug.git
## qutebrowser dependencies
git+https://github.com/tartley/colorama.git
-hg+https://bitbucket.org/cthedot/cssutils
git+https://github.com/pallets/jinja.git
git+https://github.com/pallets/markupsafe.git
hg+http://bitbucket.org/birkenfeld/pygments-main
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 3497a7937..bd77427d4 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -1,55 +1,63 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==20.1.0
-beautifulsoup4==4.9.1
-certifi==2020.6.20
+apipkg==1.5
+attrs==20.3.0
+beautifulsoup4==4.9.3
+certifi==2020.11.8
chardet==3.0.4
-cheroot==8.4.5
+cheroot==8.4.7
click==7.1.2
-# colorama==0.4.3
-coverage==5.2.1
+# colorama==0.4.4
+coverage==5.3
EasyProcess==0.3
+execnet==1.7.1
+filelock==3.0.12
Flask==1.1.2
glob2==0.7
-hunter==3.2.1
-hypothesis==5.30.0
+hunter==3.3.1
+hypothesis==5.41.4
+icdiff==1.9.1
idna==2.10
-iniconfig==1.0.1
+iniconfig==1.1.1
itsdangerous==1.1.0
-jaraco.functools==3.0.1 ; python_version>="3.6"
+jaraco.functools==3.0.1
# Jinja2==2.11.2
Mako==1.1.3
manhole==1.6.0
# MarkupSafe==1.1.1
-more-itertools==8.5.0
-packaging==20.4
-parse==1.17.0
+more-itertools==8.6.0
+packaging==20.7
+parse==1.18.0
parse-type==0.5.2
pluggy==0.13.1
+pprintpp==0.4.0
py==1.9.0
py-cpuinfo==7.0.0
-Pygments==2.6.1
+Pygments==2.7.2
pyparsing==2.4.7
-pytest==6.0.1
-pytest-bdd==3.4.0
+pytest==6.1.2
+pytest-bdd==4.0.1
pytest-benchmark==3.2.3
+pytest-clarity==0.3.0a0
pytest-cov==2.10.1
+pytest-forked==1.3.0
+pytest-icdiff==0.5
pytest-instafail==0.4.2
pytest-mock==3.3.1
pytest-qt==3.3.0
-pytest-repeat==0.8.0
-pytest-rerunfailures==9.1
+pytest-repeat==0.9.1
+pytest-rerunfailures==9.1.1
+pytest-xdist==2.1.0
pytest-xvfb==2.0.0
PyVirtualDisplay==1.3.2
-requests==2.24.0
+requests==2.25.0
requests-file==1.5.1
six==1.15.0
-sortedcontainers==2.2.2
+sortedcontainers==2.3.0
soupsieve==2.0.1
-tldextract==2.2.3
-toml==0.10.1
-urllib3==1.25.10
-vulture==2.1 ; python_version>="3.6"
+termcolor==1.1.0
+tldextract==3.1.0
+toml==0.10.2
+urllib3==1.25.11
+vulture==2.1
Werkzeug==1.0.1
-jaraco.functools==2.0; python_version<"3.6"
-vulture==1.6; python_version<"3.6"
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index f063a3512..0c9e3928f 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -1,5 +1,6 @@
beautifulsoup4
-cheroot
+# https://github.com/cherrypy/cheroot/issues/341
+cheroot!=8.4.8
coverage
Flask
hypothesis
@@ -25,14 +26,15 @@ pytest-cov
# To avoid windows from popping up
pytest-xvfb
PyVirtualDisplay
+# To run on multiple cores with -n
+pytest-xdist
+# For nicer output
+pytest-icdiff
+pytest-clarity
# Needed to test misc/userscripts/qute-lastpass
tldextract
-
-#@ markers: jaraco.functools python_version>="3.6"
-#@ add: jaraco.functools==2.0; python_version<"3.6"
-
-#@ markers: vulture python_version>="3.6"
-#@ add: vulture==1.6; python_version<"3.6"
+# https://github.com/urllib3/urllib3/issues/2071
+urllib3!=1.26.0,!=1.26.1,!=1.26.2
#@ ignore: Jinja2, MarkupSafe, colorama
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 3fb7595ad..86b3997f4 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -3,13 +3,11 @@
appdirs==1.4.4
distlib==0.3.1
filelock==3.0.12
-packaging==20.4
+packaging==20.7
pluggy==0.13.1
py==1.9.0
pyparsing==2.4.7
six==1.15.0
-toml==0.10.1
-tox==3.19.0
-tox-pip-version==0.0.7
-tox-venv==0.4.0
-virtualenv==20.0.31
+toml==0.10.2
+tox==3.20.1
+virtualenv==20.2.1
diff --git a/misc/requirements/requirements-tox.txt-raw b/misc/requirements/requirements-tox.txt-raw
index fab438034..053148f84 100644
--- a/misc/requirements/requirements-tox.txt-raw
+++ b/misc/requirements/requirements-tox.txt-raw
@@ -1,3 +1 @@
tox
-tox-venv
-tox-pip-version
diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt
index 70848d8ef..e0ea82ea2 100644
--- a/misc/requirements/requirements-vulture.txt
+++ b/misc/requirements/requirements-vulture.txt
@@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-toml==0.10.1
+toml==0.10.2
vulture==2.1
diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt
index 0ea42bf7c..1d758395c 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.0
+pathspec==0.8.1
PyYAML==5.3.1
-yamllint==1.24.2
+yamllint==1.25.0
diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md
index 729e63f6e..669bfa664 100644
--- a/misc/userscripts/README.md
+++ b/misc/userscripts/README.md
@@ -24,7 +24,7 @@ The following userscripts are included in the current directory.
- [qutedmenu](./qutedmenu): Handle open -s && open -t with bemenu.
- [readability](./readability): Executes python-readability on current page and
opens the summary as new tab.
-- [readability-js](./readability-js): Processes the current page with the readability
+- [readability-js](./readability-js): Processes the current page with the readability
library used in Firefox Reader View and opens the summary as new tab.
- [ripbang](./ripbang): Adds DuckDuckGo bang as searchengine.
- [rss](./rss): Keeps track of URLs in RSS feeds and opens new ones.
@@ -32,6 +32,9 @@ The following userscripts are included in the current directory.
- [tor_identity](./tor_identity): Change your tor identity.
- [view_in_mpv](./view_in_mpv): Views the current web page in mpv using
sensible mpv-flags.
+- [qr](./qr): Show a QR code for the current webpage via
+ [qrencode](https://fukuchi.org/works/qrencode/).
+- [kodi](./kodi): Play videos in Kodi.
[castnow]: https://github.com/xat/castnow
[youtube-dl]: https://rg3.github.io/youtube-dl/
@@ -40,7 +43,7 @@ The following userscripts are included in the current directory.
The following userscripts can be found on their own repositories.
-- [qurlshare](https://github.com/sim590/qurlshare): *secure* sharing of an URL between qutebrowser
+- [qurlshare](https://github.com/sim590/qurlshare): *secure* sharing of a URL between qutebrowser
instances using a distributed hash table.
- [qutebrowser-userscripts](https://github.com/cryzed/qutebrowser-userscripts):
a small pack of userscripts.
@@ -63,6 +66,12 @@ The following userscripts can be found on their own repositories.
snippets on web pages to the clipboard via hints.
- [Qute-Translate](https://github.com/AckslD/Qute-Translate): Translate URLs or
selections via Google Translate.
+- [qute-snippets](https://github.com/Aledosim/qute-snippets): Bind text snippets to a keyword
+ and retrieve they when you want.
+- [doi](https://github.com/cadadr/configuration/blob/master/qutebrowser/userscripts/doi):
+ Opens DOIs on Sci-Hub.
+- [1password](https://github.com/tomoakley/dotfiles/blob/master/qutebrowser/userscripts/1password):
+ Integration with 1password on macOS.
[Zotero]: https://www.zotero.org/
[Pocket]: https://getpocket.com/
diff --git a/misc/userscripts/cast b/misc/userscripts/cast
index f7b64df70..8bbf05a40 100755
--- a/misc/userscripts/cast
+++ b/misc/userscripts/cast
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
-# Behaviour
+# Behavior
# Userscript for qutebrowser which casts the url passed in $1 to the default
# ChromeCast device in the network using the program `castnow`
#
diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser
index 84be1b619..57bdb805c 100755
--- a/misc/userscripts/dmenu_qutebrowser
+++ b/misc/userscripts/dmenu_qutebrowser
@@ -38,9 +38,10 @@
# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
#
-[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
-url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser)
+[ -z "$QUTE_URL" ] && QUTE_URL='https://duckduckgo.com'
+
+url=$(printf "%s\n%s" "$QUTE_URL" "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')" | cat "$QUTE_CONFIG_DIR/quickmarks" - | dmenu -l 15 -p qutebrowser)
url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url")
[ -z "${url// }" ] && exit
diff --git a/misc/userscripts/format_json b/misc/userscripts/format_json
index 541408c70..8a83c25fa 100755
--- a/misc/userscripts/format_json
+++ b/misc/userscripts/format_json
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -euo pipefail
#
# Behavior:
diff --git a/misc/userscripts/kodi b/misc/userscripts/kodi
new file mode 100755
index 000000000..63fcc81fe
--- /dev/null
+++ b/misc/userscripts/kodi
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+#
+# Behavior:
+# A qutebrowser userscript that plays Twitch, YouTube or Vimeo videos in Kodi via its
+# API.
+#
+# Requirements:
+# awk
+# bash
+# curl
+#
+# Kodi setup:
+# Settings -> Services -> Control
+# enable 'Allow remote control via HTTP'
+# set Username and Password
+# enable 'Allow remote control from applications on this system'
+# Optional yet recommended, setup SSL within Kodi over via a proxy webserver
+#
+# userscript setup:
+# create ~/.config/qutebrowser/kodi_rc with host and authentication information like:
+#
+# HOST="http://127.0.0.1:8080"
+# or
+# HOST="https://kodi.example.com"
+#
+# AUTH="user:password"
+# or
+# AUTH="bas64authenticationinformation"
+#
+# The base64 authentication is the output of
+# `echo -ne "user:password" |base64 --wrap 0`
+# reminder base64 is not encryption
+#
+# For vim users you might want to add '# vim: set nospell filetype=bash' to the
+# kodi_rc file.
+#
+# qutebrowser setup:
+# in ~/.config/qutebrowser/config.py add something like
+#
+# to send video link via hints:
+# config.bind('X', 'hint links userscript kodi')
+# to send current URL:
+# config.bind('X', 'spawn --userscript kodi')
+#
+# troubleshooting:
+# Errors detected within this userscript with have an exit of 231. All other exit
+# codes will come from curl or awk. To test that the kodi_rc file is set up
+# correctly, run the following command. It will display a 'It works!' notification within Kodi.
+#
+# source ~/.config/qutebrowser/kodi_rc ; curl --request POST "$HOST"/jsonrpc --header "Authorization: Basic $AUTH" --header "Content-Type: application/json" --data '{"id":1,"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"It works!","message":"both HOST and AUTH are correct"}}'
+#
+# In case you miss the notification in Kodi the successful response is:
+#
+# {"id":1,"jsonrpc":"2.0","result":"OK"}
+#
+# Note, curl will display errors for some problems, but not all.
+
+if [[ -z "$QUTE_FIFO" ]] ; then
+ echo "This script is designed to run as a qutebrowser userscript, not as a standalone script."
+ exit 231
+fi
+
+# configuration loading adapted from the password_fill userscript
+QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/}
+KODI_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/kodi_rc}
+if [[ -f "$KODI_CONFIG" ]] ; then
+ # shellcheck source=/dev/null
+ source "$KODI_CONFIG"
+ if [[ -z "$HOST" || -z "$AUTH" ]] ; then
+ echo "message-error 'HOST and/or AUTH not set in $KODI_CONFIG'" > "$QUTE_FIFO"
+ exit 231
+ fi
+else
+ echo "message-error '$KODI_CONFIG not found'" > "$QUTE_FIFO"
+ exit 231
+fi
+
+# get real URL from twitter links
+if [[ "$QUTE_URL" =~ ^https:\/\/t\.co ]] ; then
+ QUTE_URL=$(curl -o /dev/null --silent --head --write-out '%{redirect_url}' "$QUTE_URL" )
+fi
+
+# regex from https://github.com/dirkjanm/firefox-send-to-xbmc/blob/master/webextension/main.js
+if [[ "$QUTE_URL" =~ ^.*twitch.tv\/([a-zA-Z0-9_]+)$ ]] ; then
+ NAME="${BASH_REMATCH[1]}"
+ JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.twitch/?mode=play&channel_name='$NAME'"}},"id":"2"}'
+
+elif [[ "$QUTE_URL" =~ ^.*twitch.tv\/videos\/([0-9]+)$ ]] ; then
+ NAME="${BASH_REMATCH[1]}"
+ JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.twitch/?mode=play&video_id='$NAME'"}},"id":"2"}'
+
+elif [[ "$QUTE_URL" =~ ^.*vimeo.com\/([0-9]+) ]] ; then
+ NAME="${BASH_REMATCH[1]}"
+ JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.vimeo/play/?video_id='$NAME'"}},"id":"2"}'
+
+elif [[ "$QUTE_URL" =~ ^.*youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=([^#\&\?]*).* ]] ; then
+ NAME="${BASH_REMATCH[1]}"
+ JSON='{"jsonrpc":"2.0","method":"Player.Open","params":{"item":{"file":"plugin://plugin.video.youtube/play/?video_id='$NAME'"}},"id":"2"}'
+fi
+
+if [[ "$JSON" ]] ; then
+ curl \
+ --request POST "$HOST"/jsonrpc \
+ --header "Authorization: Basic $AUTH" \
+ --header "Content-Type: application/json" \
+ --data "$JSON" \
+ --silent > /dev/null
+else
+ URL=$(echo "$QUTE_URL" |awk -F/ '{print $3}')
+ echo "message-warning 'kodi userscript does not support this $URL'" > "$QUTE_FIFO"
+fi
diff --git a/misc/userscripts/qr b/misc/userscripts/qr
new file mode 100755
index 000000000..84215249b
--- /dev/null
+++ b/misc/userscripts/qr
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+pngfile=$(mktemp --suffix=.png)
+trap 'rm -f "$pngfile"' EXIT
+
+qrencode -t PNG -o "$pngfile" -s 10 "$QUTE_URL"
+echo ":open -t file:///$pngfile" >> "$QUTE_FIFO"
+sleep 1 # give qutebrowser time to open the file before it gets removed
diff --git a/misc/userscripts/qute-bitwarden b/misc/userscripts/qute-bitwarden
index d5c4b1e2d..ca9d646e4 100755
--- a/misc/userscripts/qute-bitwarden
+++ b/misc/userscripts/qute-bitwarden
@@ -281,7 +281,7 @@ def main(arguments):
qute_command('enter-mode insert')
# If it finds a TOTP code, it copies it to the clipboard,
- # which is the same behaviour as the Firefox add-on.
+ # which is the same behavior as the Firefox add-on.
if not arguments.totp_only and totp and arguments.totp:
# The import is done here, to make pyperclip an optional dependency
import pyperclip
diff --git a/misc/userscripts/qute-pass b/misc/userscripts/qute-pass
index 9d078e94f..b49e87dd8 100755
--- a/misc/userscripts/qute-pass
+++ b/misc/userscripts/qute-pass
@@ -61,11 +61,19 @@ import sys
import tldextract
+
+def expanded_path(path):
+ # Expand potential ~ in paths, since this script won't be called from a shell that does it for us
+ expanded = os.path.expanduser(path)
+ # Add trailing slash if not present
+ return os.path.join(expanded, '')
+
+
argument_parser = argparse.ArgumentParser(description=__doc__, usage=USAGE, epilog=EPILOG)
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
argument_parser.add_argument('--password-store', '-p',
- default=os.getenv('PASSWORD_STORE_DIR', default=os.path.expanduser('~/.password-store')),
- help='Path to your pass password-store (only used in pass-mode)')
+ default=expanded_path(os.getenv('PASSWORD_STORE_DIR', default='~/.password-store')),
+ 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('--username-pattern', '-u', default=r'.*/(.+)',
@@ -107,7 +115,7 @@ def qute_command(command):
fifo.flush()
-def find_pass_candidates(domain, password_store_path):
+def find_pass_candidates(domain):
candidates = []
if arguments.mode == "gopass":
@@ -117,13 +125,13 @@ def find_pass_candidates(domain, password_store_path):
if domain in password:
candidates.append(password)
else:
- for path, directories, file_names in os.walk(password_store_path, followlinks=True):
+ for path, directories, file_names in os.walk(arguments.password_store, followlinks=True):
secrets = fnmatch.filter(file_names, '*.gpg')
if not secrets:
continue
# Strip password store path prefix to get the relative pass path
- pass_path = path[len(password_store_path):]
+ pass_path = path[len(arguments.password_store):]
split_path = pass_path.split(os.path.sep)
for secret in secrets:
secret_base = os.path.splitext(secret)[0]
@@ -134,25 +142,27 @@ def find_pass_candidates(domain, password_store_path):
return candidates
-def _run_pass(pass_arguments, encoding):
+def _run_pass(pass_arguments):
# The executable is conveniently named after it's mode [pass|gopass].
pass_command = [arguments.mode]
- process = subprocess.run(pass_command + pass_arguments, stdout=subprocess.PIPE)
- return process.stdout.decode(encoding).strip()
+ env = os.environ.copy()
+ env['PASSWORD_STORE_DIR'] = arguments.password_store
+ process = subprocess.run(pass_command + pass_arguments, env=env, stdout=subprocess.PIPE)
+ return process.stdout.decode(arguments.io_encoding).strip()
-def pass_(path, encoding):
- return _run_pass([path], encoding)
+def pass_(path):
+ return _run_pass(['show', path])
-def pass_otp(path, encoding):
- return _run_pass(['otp', path], encoding)
+def pass_otp(path):
+ return _run_pass(['otp', path])
-def dmenu(items, invocation, encoding):
+def dmenu(items, invocation):
command = shlex.split(invocation)
- process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE)
- return process.stdout.decode(encoding).strip()
+ process = subprocess.run(command, input='\n'.join(items).encode(arguments.io_encoding), stdout=subprocess.PIPE)
+ return process.stdout.decode(arguments.io_encoding).strip()
def fake_key_raw(text):
@@ -170,11 +180,6 @@ def main(arguments):
extractor = tldextract.TLDExtract(extra_suffixes=arguments.extra_url_suffixes.split(','))
extract_result = extractor(arguments.url)
- # Expand potential ~ in paths, since this script won't be called from a shell that does it for us
- password_store_path = os.path.expanduser(arguments.password_store)
- # Add trailing slash if not present
- password_store_path = os.path.join(password_store_path, '')
-
# Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
# the registered domain name, the IPv4 address if that's what the URL represents and finally the private domain
# (if a non-public suffix was used).
@@ -188,7 +193,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, password_store_path)
+ target_candidates = find_pass_candidates(target)
if not target_candidates:
continue
@@ -200,8 +205,7 @@ 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,
- arguments.io_encoding)
+ selection = candidates.pop() if len(candidates) == 1 else dmenu(sorted(candidates), arguments.dmenu_invocation)
# Nothing was selected, simply return
if not selection:
return ExitCodes.SUCCESS
@@ -209,7 +213,7 @@ def main(arguments):
# If username-target is path and user asked for username-only, we don't need to run pass
secret = None
if not (arguments.username_target == 'path' and arguments.username_only):
- secret = pass_(selection, arguments.io_encoding)
+ secret = pass_(selection)
# Match password
match = re.match(arguments.password_pattern, secret)
@@ -231,7 +235,7 @@ def main(arguments):
elif arguments.password_only:
fake_key_raw(password)
elif arguments.otp_only:
- otp = pass_otp(selection, arguments.io_encoding)
+ otp = pass_otp(selection)
fake_key_raw(otp)
else:
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
diff --git a/misc/userscripts/qutedmenu b/misc/userscripts/qutedmenu
index cc5a44413..bdd0d9b27 100755
--- a/misc/userscripts/qutedmenu
+++ b/misc/userscripts/qutedmenu
@@ -6,8 +6,9 @@
# If you would like to set a custom colorscheme/font use these dirs.
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors
-readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
+
+readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
readonly optsfile=$confdir/dmenu/bemenucolors
create_menu() {
@@ -22,15 +23,13 @@ create_menu() {
done < "$QUTE_CONFIG_DIR"/bookmarks/urls
# Finally history
- while read -r _ url; do
- printf -- '%s\n' "$url"
- done < "$QUTE_DATA_DIR"/history
+ printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')"
}
get_selection() {
opts+=(-p qutebrowser)
- #create_menu | dmenu -l 10 "${opts[@]}"
- create_menu | bemenu -l 10 "${opts[@]}"
+ create_menu | dmenu -l 10 "${opts[@]}"
+ #create_menu | bemenu -l 10 "${opts[@]}"
}
# Main
diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js
index 4b9446ed5..310d1c081 100755
--- a/misc/userscripts/readability-js
+++ b/misc/userscripts/readability-js
@@ -44,6 +44,45 @@ const HEADER = `
h1, h2, h3 {
line-height: 1.2;
}
+ img {
+ max-width:100%;
+ height:auto;
+ }
+ p > img:only-child,
+ p > a:only-child > img:only-child,
+ .wp-caption img,
+ figure img {
+ display: block;
+ }
+ table,
+ th,
+ td {
+ border: 1px solid grey;
+ border-collapse: collapse;
+ padding: 6px;
+ vertical-align: top;
+ }
+ table {
+ margin: 5px;
+ }
+ pre {
+ padding: 16px;
+ overflow: auto;
+ line-height: 1.45;
+ background-color: #dddddd;
+ }
+ code {
+ padding: .2em .4em;
+ margin: 0;
+ background-color: #dddddd;
+ }
+ blockquote {
+ border-inline-start: 2px solid grey !important;
+ padding: 0;
+ padding-inline-start: 16px;
+ margin-inline-start: 24px;
+ border-radius: 5px;
+ }
</style>
<!-- This icon is licensed under the Mozilla Public License 2.0 (available at: https://www.mozilla.org/en-US/MPL/2.0/).
The original icon can be found here: https://dxr.mozilla.org/mozilla-central/source/browser/themes/shared/reader/readerMode.svg -->
@@ -58,13 +97,24 @@ const HEADER = `
</head>`;
const scriptsDir = path.join(process.env.QUTE_DATA_DIR, 'userscripts');
const tmpFile = path.join(scriptsDir, '/readability.html');
-const domOpts = {url: process.env.QUTE_URL, contentType: "text/html; charset=utf-8"};
-if (!fs.existsSync(scriptsDir)){
+if (!fs.existsSync(scriptsDir)) {
fs.mkdirSync(scriptsDir);
}
-JSDOM.fromFile(process.env.QUTE_HTML, domOpts).then(dom => {
+let getDOM, domOpts, target;
+// When hinting, use the selected hint instead of the current page
+if (process.env.QUTE_MODE === 'hints') {
+ getDOM = JSDOM.fromURL;
+ target = process.env.QUTE_URL;
+}
+else {
+ getDOM = JSDOM.fromFile;
+ domOpts = {url: process.env.QUTE_URL, contentType: "text/html; charset=utf-8"};
+ target = process.env.QUTE_HTML;
+}
+
+getDOM(target, domOpts).then(dom => {
let reader = new Readability(dom.window.document);
let article = reader.parse();
let content = util.format(HEADER, article.title) + article.content;
diff --git a/pytest.ini b/pytest.ini
index 1235efb4b..f936a02cb 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -30,12 +30,10 @@ markers =
qtwebkit_skip: Tests not applicable with QtWebKit
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine
- js_prompt: Tests needing to display a javascript prompt
this: Used to mark tests during development
no_invalid_lines: Don't fail on unparseable lines in end2end tests
- qtbug60673: Tests which are broken if the conversion from orange selection to real selection is flaky
fake_os: Fake utils.is_* to a fake operating system
- unicode_locale: Tests which need an unicode locale to work
+ unicode_locale: Tests which need a unicode locale to work
qtwebkit6021_xfail: Tests which would fail on WebKit version 602.1
js_headers: Sets JS headers dynamically on QtWebEngine (unsupported on some versions)
qtwebkit_pdf_imageformat_skip: Broken on QtWebKit with PDF image format plugin installed
@@ -76,9 +74,12 @@ qt_log_ignore =
^QPaintDevice: Cannot destroy paint device that is being painted
^DirectWrite: CreateFontFaceFromHDC\(\) failed .*
^Attribute Qt::AA_ShareOpenGLContexts must be set before QCoreApplication is created\.
+ ^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not de-queue request, failed to report HostNotFoundError
+ ^The available OpenGL surface format was either not version 3\.2 or higher or not a Core Profile.*
xfail_strict = true
filterwarnings =
error
# See https://github.com/HypothesisWorks/hypothesis/issues/2370
ignore:.*which is reset between function calls but not between test cases generated by:hypothesis.errors.HypothesisDeprecationWarning
+ default:Test process .* failed to terminate!:UserWarning
faulthandler_timeout = 90
diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py
index 34799df17..b5b4b8c7c 100644
--- a/qutebrowser/__init__.py
+++ b/qutebrowser/__init__.py
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2020 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
-__version__ = "1.13.1"
+__version__ = "1.14.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/api/cmdutils.py b/qutebrowser/api/cmdutils.py
index 5d74991c1..d0d69a04c 100644
--- a/qutebrowser/api/cmdutils.py
+++ b/qutebrowser/api/cmdutils.py
@@ -45,12 +45,12 @@ Possible values:
value.
- A python enum type: All members of the enum are possible values.
- A ``typing.Union`` of multiple types above: Any of these types are valid
- values, e.g., ``typing.Union[str, int]``.
+ values, e.g., ``Union[str, int]``.
"""
import inspect
-import typing
+from typing import Any, Callable, Iterable
from qutebrowser.utils import qtutils
from qutebrowser.commands import command, cmdexc
@@ -91,8 +91,7 @@ def check_overflow(arg: int, ctype: str) -> None:
"representation.".format(ctype))
-def check_exclusive(flags: typing.Iterable[bool],
- names: typing.Iterable[str]) -> None:
+def check_exclusive(flags: Iterable[bool], names: Iterable[str]) -> None:
"""Check if only one flag is set with exclusive flags.
Raise a CommandError if not.
@@ -113,7 +112,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
def __init__(self, *,
instance: str = None,
name: str = None,
- **kwargs: typing.Any) -> None:
+ **kwargs: Any) -> None:
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
@@ -128,7 +127,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
# The arguments to pass to Command.
self._kwargs = kwargs
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
"""Register the command before running the function.
Gets called when a function should be decorated.
@@ -175,7 +174,7 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name
def foo(bar: str):
...
- For ``typing.Union`` types, the given ``choices`` are only checked if other
+ For ``Union`` types, the given ``choices`` are only checked if other
types (like ``int``) don't match.
The following arguments are supported for ``@cmdutils.argument``:
@@ -197,11 +196,11 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name
trailing underscores stripped and underscores replaced by dashes.
"""
- def __init__(self, argname: str, **kwargs: typing.Any) -> None:
+ def __init__(self, argname: str, **kwargs: Any) -> None:
self._argname = argname # The name of the argument to handle.
self._kwargs = kwargs # Valid ArgInfo members.
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
funcname = func.__name__
if self._argname not in inspect.signature(func).parameters:
diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py
index 3b84a999c..fb363d858 100644
--- a/qutebrowser/api/config.py
+++ b/qutebrowser/api/config.py
@@ -19,7 +19,7 @@
"""Access to the qutebrowser configuration."""
-import typing
+from typing import cast, Any
from PyQt5.QtCore import QUrl
@@ -35,9 +35,9 @@ from qutebrowser.config import config
#: This also supports setting configuration values::
#:
#: config.val.content.javascript.enabled = False
-val = typing.cast('config.ConfigContainer', None)
+val = cast('config.ConfigContainer', None)
-def get(name: str, url: QUrl = None) -> typing.Any:
+def get(name: str, url: QUrl = None) -> Any:
"""Get a value from the config based on a string name."""
return config.instance.get(name, url)
diff --git a/qutebrowser/api/downloads.py b/qutebrowser/api/downloads.py
index 5e5d1916a..55656c5b5 100644
--- a/qutebrowser/api/downloads.py
+++ b/qutebrowser/api/downloads.py
@@ -75,4 +75,6 @@ def download_temp(url: QUrl) -> TempDownload:
fobj.name = 'temporary: ' + url.host()
target = downloads.FileObjDownloadTarget(fobj)
download_manager = objreg.get('qtnetwork-download-manager')
- return download_manager.get(url, target=target, auto_remove=True)
+ # cache=False is set as a WORKAROUND for MS Defender thinking we're a trojan
+ # downloader when caching the hostblock list...
+ return download_manager.get(url, target=target, auto_remove=True, cache=False)
diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py
index 9bd14a8a1..4eadb2a99 100644
--- a/qutebrowser/api/hook.py
+++ b/qutebrowser/api/hook.py
@@ -22,13 +22,13 @@
"""Hooks for extensions."""
import importlib
-import typing
+from typing import Callable
from qutebrowser.extensions import loader
-def _add_module_info(func: typing.Callable) -> loader.ModuleInfo:
+def _add_module_info(func: Callable) -> loader.ModuleInfo:
"""Add module info to the given function."""
module = importlib.import_module(func.__module__)
return loader.add_module_info(module)
@@ -48,7 +48,7 @@ class init:
message.info("Extension initialized.")
"""
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
info = _add_module_info(func)
if info.init_hook is not None:
raise ValueError("init hook is already registered!")
@@ -86,7 +86,7 @@ class config_changed:
def __init__(self, option_filter: str = None) -> None:
self._filter = option_filter
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
info = _add_module_info(func)
info.config_changed_hooks.append((self._filter, func))
return func
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 8eecdb1da..bc554bfc0 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -43,7 +43,7 @@ import functools
import tempfile
import datetime
import argparse
-import typing
+from typing import Iterable, Optional, cast
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon
@@ -74,7 +74,7 @@ from qutebrowser.misc import utilcmds
# pylint: enable=unused-import
-q_app = typing.cast(QApplication, None)
+q_app = cast(QApplication, None)
def run(args):
@@ -82,6 +82,8 @@ def run(args):
if args.temp_basedir:
args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-')
+ log.init.debug("Main process PID: {}".format(os.getpid()))
+
log.init.debug("Initializing directories...")
standarddir.init(args)
utils.preload_resources()
@@ -198,7 +200,7 @@ def _init_pulseaudio():
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85363
Affected Qt versions:
- - Older than 5.11
+ - Older than 5.11 (which is unsupported)
- 5.14.0 to 5.15.0 (inclusive)
However, we set this on all versions so that qutebrowser's icon gets picked
@@ -255,7 +257,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
if command_target in {'window', 'private-window'}:
command_target = 'tab-silent'
- win_id = None # type: typing.Optional[int]
+ win_id: Optional[int] = None
if via_ipc and not args:
win_id = mainwindow.get_window(via_ipc=via_ipc,
@@ -324,7 +326,7 @@ def _open_startpage(win_id=None):
If set, open the startpage in the given window.
"""
if win_id is not None:
- window_ids = [win_id] # type: typing.Iterable[int]
+ window_ids: Iterable[int] = [win_id]
else:
window_ids = objreg.window_registry
for cur_win_id in list(window_ids): # Copying as the dict could change
@@ -365,10 +367,6 @@ def _open_special_pages(args):
objects.backend == usertypes.Backend.QtWebKit,
'qute://warning/webkit'),
- ('old-qt-warning-shown',
- not qtutils.version_check('5.11'),
- 'qute://warning/old-qt'),
-
('session-warning-shown',
qtutils.version_check('5.15', compiled=False),
'qute://warning/sessions'),
@@ -535,7 +533,9 @@ class Application(QApplication):
self.launch_time = datetime.datetime.now()
self.focusObjectChanged.connect( # type: ignore[attr-defined]
self.on_focus_object_changed)
+
self.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
+ self.setAttribute(Qt.AA_MacDontSwapCtrlAndMeta, True)
self.new_window.connect(self._on_new_window)
@@ -554,15 +554,15 @@ class Application(QApplication):
def event(self, e):
"""Handle macOS FileOpen events."""
- if e.type() == QEvent.FileOpen:
- url = e.url()
- if url.isValid():
- open_url(url, no_raise=True)
- else:
- message.error("Invalid URL: {}".format(url.errorString()))
- else:
+ if e.type() != QEvent.FileOpen:
return super().event(e)
+ url = e.url()
+ if url.isValid():
+ open_url(url, no_raise=True)
+ else:
+ message.error("Invalid URL: {}".format(url.errorString()))
+
return True
def __repr__(self):
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index b7b2f3d91..dd21cd0e0 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -22,7 +22,8 @@
import enum
import itertools
import functools
-import typing
+from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional,
+ Sequence, Set, Type, Union)
import attr
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt,
@@ -32,9 +33,10 @@ from PyQt5.QtWidgets import QWidget, QApplication, QDialog
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtNetwork import QNetworkAccessManager
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from PyQt5.QtWebKit import QWebHistory
- from PyQt5.QtWebEngineWidgets import QWebEngineHistory
+ from PyQt5.QtWebKitWidgets import QWebPage
+ from PyQt5.QtWebEngineWidgets import QWebEngineHistory, QWebEnginePage
import pygments
import pygments.lexers
@@ -48,7 +50,7 @@ from qutebrowser.misc import miscwidgets, objects, sessions
from qutebrowser.browser import eventfilter, inspector
from qutebrowser.qt import sip
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser import webelem
from qutebrowser.browser.inspector import AbstractWebInspector
@@ -71,7 +73,7 @@ def create(win_id: int,
mode_manager = modeman.instance(win_id)
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
- tab_class = webenginetab.WebEngineTab # type: typing.Type[AbstractTab]
+ tab_class: Type[AbstractTab] = webenginetab.WebEngineTab
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkittab
tab_class = webkittab.WebKitTab
@@ -100,13 +102,23 @@ class UnsupportedOperationError(WebTabError):
"""Raised when an operation is not supported with the given backend."""
-TerminationStatus = enum.Enum('TerminationStatus', [
- 'normal',
- 'abnormal', # non-zero exit status
- 'crashed', # e.g. segfault
- 'killed',
- 'unknown',
-])
+class TerminationStatus(enum.Enum):
+
+ """How a QtWebEngine renderer process terminated.
+
+ Also see QWebEnginePage::RenderProcessTerminationStatus
+ """
+
+ #: Unknown render process status value gotten from Qt.
+ unknown = -1
+ #: The render process terminated normally.
+ normal = 0
+ #: The render process terminated with with a non-zero exit status.
+ abnormal = 1
+ #: The render process crashed, for example because of a segmentation fault.
+ crashed = 2
+ #: The render process was killed, for example by SIGKILL or task manager kill.
+ killed = 3
@attr.s
@@ -131,19 +143,17 @@ class TabData:
splitter: InspectorSplitter used to show inspector inside the tab.
"""
- keep_icon = attr.ib(False) # type: bool
- viewing_source = attr.ib(False) # type: bool
- inspector = attr.ib(None) # type: typing.Optional[AbstractWebInspector]
- open_target = attr.ib(
- usertypes.ClickTarget.normal) # type: usertypes.ClickTarget
- override_target = attr.ib(
- None) # type: typing.Optional[usertypes.ClickTarget]
- pinned = attr.ib(False) # type: bool
- fullscreen = attr.ib(False) # type: bool
- netrc_used = attr.ib(False) # type: bool
- input_mode = attr.ib(usertypes.KeyMode.normal) # type: usertypes.KeyMode
- last_navigation = attr.ib(None) # type: usertypes.NavigationRequest
- splitter = attr.ib(None) # type: miscwidgets.InspectorSplitter
+ keep_icon: bool = attr.ib(False)
+ viewing_source: bool = attr.ib(False)
+ inspector: Optional['AbstractWebInspector'] = attr.ib(None)
+ open_target: usertypes.ClickTarget = attr.ib(usertypes.ClickTarget.normal)
+ override_target: Optional[usertypes.ClickTarget] = attr.ib(None)
+ pinned: bool = attr.ib(False)
+ fullscreen: bool = attr.ib(False)
+ netrc_used: bool = attr.ib(False)
+ input_mode: usertypes.KeyMode = attr.ib(usertypes.KeyMode.normal)
+ last_navigation: usertypes.NavigationRequest = attr.ib(None)
+ splitter: miscwidgets.InspectorSplitter = attr.ib(None)
def should_show_icon(self) -> bool:
return (config.val.tabs.favicons.show == 'always' or
@@ -154,13 +164,11 @@ class AbstractAction:
"""Attribute ``action`` of AbstractTab for Qt WebActions."""
- # The class actions are defined on (QWeb{Engine,}Page)
- action_class = None # type: type
- # The type of the actions (QWeb{Engine,}Page.WebAction)
- action_base = None # type: type
+ action_class: Type[Union['QWebPage', 'QWebEnginePage']]
+ action_base: Type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']]
def __init__(self, tab: 'AbstractTab') -> None:
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._tab = tab
def exit_fullscreen(self) -> None:
@@ -211,7 +219,7 @@ class AbstractPrinting:
"""Attribute ``printing`` of AbstractTab for printing the page."""
def __init__(self, tab: 'AbstractTab') -> None:
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._tab = tab
def check_pdf_support(self) -> None:
@@ -222,14 +230,6 @@ class AbstractPrinting:
"""
raise NotImplementedError
- def check_printer_support(self) -> None:
- """Check whether writing to a printer is supported.
-
- If it's not supported (by the current Qt version), a WebTabError is
- raised.
- """
- raise NotImplementedError
-
def check_preview_support(self) -> None:
"""Check whether showing a print preview is supported.
@@ -243,7 +243,7 @@ class AbstractPrinting:
raise NotImplementedError
def to_printer(self, printer: QPrinter,
- callback: typing.Callable[[bool], None] = None) -> None:
+ callback: Callable[[bool], None] = None) -> None:
"""Print the tab.
Args:
@@ -255,8 +255,6 @@ class AbstractPrinting:
def show_dialog(self) -> None:
"""Print with a QPrintDialog."""
- self.check_printer_support()
-
def print_callback(ok: bool) -> None:
"""Called when printing finished."""
if not ok:
@@ -295,13 +293,13 @@ class AbstractSearch(QObject):
#: Signal emitted when an existing search was cleared.
cleared = pyqtSignal()
- _Callback = typing.Callable[[bool], None]
+ _Callback = Callable[[bool], None]
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
- self.text = None # type: typing.Optional[str]
+ self._widget = cast(QWidget, None)
+ self.text: Optional[str] = None
self.search_displayed = False
def _is_case_sensitive(self, ignore_case: usertypes.IgnoreCase) -> bool:
@@ -364,7 +362,7 @@ class AbstractZoom(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
# Whether zoom was changed from the default.
self._default_zoom_changed = False
self._init_neighborlist()
@@ -384,9 +382,8 @@ class AbstractZoom(QObject):
It is a NeighborList with the zoom levels."""
levels = config.val.zoom.levels
- self._neighborlist = usertypes.NeighborList(
- levels, mode=usertypes.NeighborList.Modes.edge
- ) # type: usertypes.NeighborList[float]
+ self._neighborlist: usertypes.NeighborList[float] = usertypes.NeighborList(
+ levels, mode=usertypes.NeighborList.Modes.edge)
self._neighborlist.fuzzyval = config.val.zoom.default
def apply_offset(self, offset: int) -> float:
@@ -440,9 +437,9 @@ class SelectionState(enum.Enum):
NOTE: Names need to line up with SelectionState in caret.js!
"""
- none = 1
- normal = 2
- line = 3
+ none = enum.auto()
+ normal = enum.auto()
+ line = enum.auto()
class AbstractCaret(QObject):
@@ -455,14 +452,15 @@ class AbstractCaret(QObject):
follow_selected_done = pyqtSignal()
def __init__(self,
+ tab: 'AbstractTab',
mode_manager: modeman.ModeManager,
parent: QWidget = None) -> None:
super().__init__(parent)
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._mode_manager = mode_manager
mode_manager.entered.connect(self._on_mode_entered)
mode_manager.left.connect(self._on_mode_left)
- # self._tab is set by subclasses so mypy knows its concrete type.
+ self._tab = tab
def _on_mode_entered(self, mode: usertypes.KeyMode) -> None:
raise NotImplementedError
@@ -521,7 +519,7 @@ class AbstractCaret(QObject):
def drop_selection(self) -> None:
raise NotImplementedError
- def selection(self, callback: typing.Callable[[str], None]) -> None:
+ def selection(self, callback: Callable[[str], None]) -> None:
raise NotImplementedError
def reverse_selection(self) -> None:
@@ -551,7 +549,7 @@ class AbstractScroller(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
if 'log-scroll-pos' in objects.debug_flags:
self.perc_changed.connect(self._log_scroll_pos_change)
@@ -619,11 +617,6 @@ class AbstractHistoryPrivate:
"""Private API related to the history."""
- def __init__(self, tab: 'AbstractTab'):
- self._tab = tab
- self._history = typing.cast(
- typing.Union['QWebHistory', 'QWebEngineHistory'], None)
-
def serialize(self) -> bytes:
"""Serialize into an opaque format understood by self.deserialize."""
raise NotImplementedError
@@ -632,7 +625,7 @@ class AbstractHistoryPrivate:
"""Deserialize from a format produced by self.serialize."""
raise NotImplementedError
- def load_items(self, items: typing.Sequence) -> None:
+ def load_items(self, items: Sequence) -> None:
"""Deserialize from a list of WebHistoryItems."""
raise NotImplementedError
@@ -643,14 +636,13 @@ class AbstractHistory:
def __init__(self, tab: 'AbstractTab') -> None:
self._tab = tab
- self._history = typing.cast(
- typing.Union['QWebHistory', 'QWebEngineHistory'], None)
- self.private_api = AbstractHistoryPrivate(tab)
+ self._history = cast(Union['QWebHistory', 'QWebEngineHistory'], None)
+ self.private_api = AbstractHistoryPrivate()
def __len__(self) -> int:
raise NotImplementedError
- def __iter__(self) -> typing.Iterable:
+ def __iter__(self) -> Iterable:
raise NotImplementedError
def _check_count(self, count: int) -> None:
@@ -687,16 +679,16 @@ class AbstractHistory:
def can_go_forward(self) -> bool:
raise NotImplementedError
- def _item_at(self, i: int) -> typing.Any:
+ def _item_at(self, i: int) -> Any:
raise NotImplementedError
- def _go_to_item(self, item: typing.Any) -> None:
+ def _go_to_item(self, item: Any) -> None:
raise NotImplementedError
- def back_items(self) -> typing.List[typing.Any]:
+ def back_items(self) -> List[Any]:
raise NotImplementedError
- def forward_items(self) -> typing.List[typing.Any]:
+ def forward_items(self) -> List[Any]:
raise NotImplementedError
@@ -704,15 +696,13 @@ class AbstractElements:
"""Finding and handling of elements on the page."""
- _MultiCallback = typing.Callable[
- [typing.Sequence['webelem.AbstractWebElement']], None]
- _SingleCallback = typing.Callable[
- [typing.Optional['webelem.AbstractWebElement']], None]
- _ErrorCallback = typing.Callable[[Exception], None]
+ _MultiCallback = Callable[[Sequence['webelem.AbstractWebElement']], None]
+ _SingleCallback = Callable[[Optional['webelem.AbstractWebElement']], None]
+ _ErrorCallback = Callable[[Exception], None]
- def __init__(self) -> None:
- self._widget = typing.cast(QWidget, None)
- # self._tab is set by subclasses so mypy knows its concrete type.
+ def __init__(self, tab: 'AbstractTab') -> None:
+ self._widget = cast(QWidget, None)
+ self._tab = tab
def find_css(self, selector: str,
callback: _MultiCallback,
@@ -772,7 +762,7 @@ class AbstractAudio(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent)
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._tab = tab
def set_muted(self, muted: bool, override: bool = False) -> None:
@@ -803,7 +793,7 @@ class AbstractTabPrivate:
def __init__(self, mode_manager: modeman.ModeManager,
tab: 'AbstractTab') -> None:
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._tab = tab
self._mode_manager = mode_manager
@@ -821,7 +811,7 @@ class AbstractTabPrivate:
return
def _auto_insert_mode_cb(
- elem: typing.Optional['webelem.AbstractWebElement']
+ elem: Optional['webelem.AbstractWebElement']
) -> None:
"""Called from JS after finding the focused element."""
if elem is None:
@@ -836,7 +826,7 @@ class AbstractTabPrivate:
def clear_ssl_errors(self) -> None:
raise NotImplementedError
- def networkaccessmanager(self) -> typing.Optional[QNetworkAccessManager]:
+ def networkaccessmanager(self) -> Optional[QNetworkAccessManager]:
"""Get the QNetworkAccessManager for this tab.
This is only implemented for QtWebKit.
@@ -897,6 +887,8 @@ class AbstractTab(QWidget):
icon_changed = pyqtSignal(QIcon)
#: Signal emitted when a page's title changed (new title as str)
title_changed = pyqtSignal(str)
+ #: Signal emitted when this tab was pinned/unpinned (new pinned state as bool)
+ pinned_changed = pyqtSignal(bool)
#: Signal emitted when a new tab should be opened (url as QUrl)
new_tab_requested = pyqtSignal(QUrl)
#: Signal emitted when a page's URL changed (url as QUrl)
@@ -926,7 +918,7 @@ class AbstractTab(QWidget):
# Note that we remember hosts here, without scheme/port:
# QtWebEngine/Chromium also only remembers hostnames, and certificates are
# for a given hostname anyways.
- _insecure_hosts = set() # type: typing.Set[str]
+ _insecure_hosts: Set[str] = set()
def __init__(self, *, win_id: int,
mode_manager: modeman.ModeManager,
@@ -946,12 +938,12 @@ class AbstractTab(QWidget):
self.data = TabData()
self._layout = miscwidgets.WrapperLayout(self)
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._progress = 0
self._load_status = usertypes.LoadStatus.none
self._tab_event_filter = eventfilter.TabEventFilter(
self, parent=self)
- self.backend = None # type: typing.Optional[usertypes.Backend]
+ self.backend: Optional[usertypes.Backend] = None
# If true, this tab has been requested to be removed (or is removed).
self.pending_removal = False
@@ -1056,9 +1048,6 @@ class AbstractTab(QWidget):
self.data.last_navigation = navigation
if not navigation.url.isValid():
- # Also a WORKAROUND for missing IDNA 2008 support in QUrl, see
- # https://bugreports.qt.io/browse/QTBUG-60364
-
if navigation.navigation_type == navigation.Type.link_clicked:
msg = urlutils.get_errstring(navigation.url,
"Invalid link clicked")
@@ -1127,14 +1116,11 @@ class AbstractTab(QWidget):
def load_status(self) -> usertypes.LoadStatus:
return self._load_status
- def _load_url_prepare(self, url: QUrl, *,
- emit_before_load_started: bool = True) -> None:
+ def _load_url_prepare(self, url: QUrl) -> None:
qtutils.ensure_valid(url)
- if emit_before_load_started:
- self.before_load_started.emit(url)
+ self.before_load_started.emit(url)
- def load_url(self, url: QUrl, *,
- emit_before_load_started: bool = True) -> None:
+ def load_url(self, url: QUrl) -> None:
raise NotImplementedError
def reload(self, *, force: bool = False) -> None:
@@ -1154,7 +1140,7 @@ class AbstractTab(QWidget):
self.send_event(release_evt)
def dump_async(self,
- callback: typing.Callable[[str], None], *,
+ callback: Callable[[str], None], *,
plain: bool = False) -> None:
"""Dump the current page's html asynchronously.
@@ -1166,8 +1152,8 @@ class AbstractTab(QWidget):
def run_js_async(
self,
code: str,
- callback: typing.Callable[[typing.Any], None] = None, *,
- world: typing.Union[usertypes.JsWorld, int] = None
+ callback: Callable[[Any], None] = None, *,
+ world: Union[usertypes.JsWorld, int] = None
) -> None:
"""Run javascript async.
@@ -1191,6 +1177,10 @@ class AbstractTab(QWidget):
def set_html(self, html: str, base_url: QUrl = QUrl()) -> None:
raise NotImplementedError
+ def set_pinned(self, pinned: bool) -> None:
+ self.data.pinned = pinned
+ self.pinned_changed.emit(pinned)
+
def __repr__(self) -> str:
try:
qurl = self.url()
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index ff18b5408..18777e250 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -22,7 +22,7 @@
import os.path
import shlex
import functools
-import typing
+from typing import cast, Callable, Dict, Union
from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery
@@ -278,7 +278,7 @@ class CommandDispatcher:
return
to_pin = not tab.data.pinned
- self._tabbed_browser.widget.set_tab_pinned(tab, to_pin)
+ tab.set_pinned(to_pin)
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window')
@@ -421,7 +421,8 @@ class CommandDispatcher:
newtab.data.keep_icon = True
newtab.history.private_api.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor())
- new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
+
+ newtab.set_pinned(curtab.data.pinned)
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window',
@@ -599,15 +600,14 @@ class CommandDispatcher:
widget = self._current_widget()
url = self._current_url()
- handlers = {
+ handlers: Dict[str, Callable] = {
'prev': functools.partial(navigate.prevnext, prev=True),
'next': functools.partial(navigate.prevnext, prev=False),
'up': navigate.path_up,
- 'decrement': functools.partial(navigate.incdec,
- inc_or_dec='decrement'),
- 'increment': functools.partial(navigate.incdec,
- inc_or_dec='increment'),
- } # type: typing.Dict[str, typing.Callable]
+ 'strip': navigate.strip,
+ 'decrement': functools.partial(navigate.incdec, inc_or_dec='decrement'),
+ 'increment': functools.partial(navigate.incdec, inc_or_dec='increment'),
+ }
try:
if where in ['prev', 'next']:
@@ -673,7 +673,7 @@ class CommandDispatcher:
url = QUrl(self._current_url())
url_query = QUrlQuery()
- url_query_str = urlutils.query_string(url)
+ url_query_str = url.query()
if '&' not in url_query_str and ';' in url_query_str:
url_query.setQueryDelimiters('=', ';')
url_query.setQuery(url_query_str)
@@ -948,7 +948,7 @@ class CommandDispatcher:
@cmdutils.argument('index', choices=['last', 'stack-next', 'stack-prev'],
completion=miscmodels.tab_focus)
@cmdutils.argument('count', value=cmdutils.Value.count)
- def tab_focus(self, index: typing.Union[str, int] = None,
+ def tab_focus(self, index: Union[str, int] = None,
count: int = None, no_last: bool = False) -> None:
"""Select the tab given as argument/[count].
@@ -992,7 +992,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['+', '-'])
@cmdutils.argument('count', value=cmdutils.Value.count)
- def tab_move(self, index: typing.Union[str, int] = None,
+ def tab_move(self, index: Union[str, int] = None,
count: int = None) -> None:
"""Move the current tab according to the argument and [count].
@@ -1057,7 +1057,8 @@ class CommandDispatcher:
verbose: Show notifications when the command started/exited.
output: Show the output in a new tab.
output_messages: Show the output as messages.
- detach: Whether the command should be detached from qutebrowser.
+ detach: Detach the command from qutebrowser so that it continues
+ running when qutebrowser quits.
cmdline: The commandline to execute.
count: Given to userscripts as $QUTE_COUNT.
"""
@@ -1304,7 +1305,7 @@ class CommandDispatcher:
if mhtml_:
raise cmdutils.CommandError("Can only download the current "
"page as mhtml.")
- url = urlutils.qurl_from_user_input(url)
+ url = QUrl.fromUserInput(url)
urlutils.raise_cmdexc_if_invalid(url)
download_manager.get(url, target=target)
elif mhtml_:
@@ -1430,7 +1431,7 @@ class CommandDispatcher:
query = QUrlQuery()
query.addQueryItem('level', level)
if plain:
- query.addQueryItem('plain', typing.cast(str, None))
+ query.addQueryItem('plain', cast(str, None))
if logfilter:
try:
@@ -1651,7 +1652,7 @@ class CommandDispatcher:
url: bool = False,
quiet: bool = False,
*,
- world: typing.Union[usertypes.JsWorld, int] = None) -> None:
+ world: Union[usertypes.JsWorld, int] = None) -> None:
"""Evaluate a JavaScript string.
Args:
@@ -1663,7 +1664,15 @@ class CommandDispatcher:
url: Interpret js-code as a `javascript:...` URL.
quiet: Don't show resulting JS object.
world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to
- run the snippet in.
+ run the snippet in. Predefined world names are:
+
+ - `main` (same world as the web page's JavaScript and
+ Greasemonkey, unless overridden via `@qute-js-world`)
+ - `application` (used for internal qutebrowser JS code,
+ should not be used via `:jseval` unless you know what
+ you're doing)
+ - `user` (currently unused)
+ - `jseval` (used for this command by default)
"""
cmdutils.check_exclusive((file, url), 'fu')
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index 3c3932c5f..96220897c 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -28,7 +28,7 @@ import functools
import pathlib
import tempfile
import enum
-import typing
+from typing import Any, Dict, IO, List, MutableSequence, Optional, Union
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel, QUrl)
@@ -49,7 +49,7 @@ class ModelRole(enum.IntEnum):
# Remember the last used directory
-last_used_directory = None # type: typing.Optional[str]
+last_used_directory: Optional[str] = None
# All REFRESH_INTERVAL milliseconds, speeds will be recalculated and downloads
# redrawn.
@@ -228,7 +228,7 @@ def suggested_fn_from_title(url_path, title=None):
ext_whitelist = [".html", ".htm", ".php", ""]
_, ext = os.path.splitext(url_path)
- suggested_fn = None # type: typing.Optional[str]
+ suggested_fn: Optional[str] = None
if ext.lower() in ext_whitelist and title:
suggested_fn = utils.sanitize_filename(title, shorten=True)
if not suggested_fn.lower().endswith((".html", ".htm")):
@@ -355,8 +355,7 @@ class DownloadItemStats(QObject):
self.speed = 0
self._last_done = 0
samples = int(self.SPEED_AVG_WINDOW * (1000 / _REFRESH_INTERVAL))
- self._speed_avg = collections.deque(
- maxlen=samples) # type: typing.MutableSequence[float]
+ self._speed_avg: MutableSequence[float] = collections.deque(maxlen=samples)
def update_speed(self):
"""Recalculate the current download speed.
@@ -459,12 +458,14 @@ class AbstractDownloadItem(QObject):
self.basename = '???'
self.successful = False
- self.fileobj = UnsupportedAttribute(
- ) # type: typing.Union[UnsupportedAttribute, typing.IO[bytes], None]
- self.raw_headers = UnsupportedAttribute(
- ) # type: typing.Union[UnsupportedAttribute, typing.Dict[bytes,bytes]]
+ self.fileobj: Union[
+ UnsupportedAttribute, IO[bytes], None
+ ] = UnsupportedAttribute()
+ self.raw_headers: Union[
+ UnsupportedAttribute, Dict[bytes, bytes]
+ ] = UnsupportedAttribute()
- self._filename = None # type: typing.Optional[str]
+ self._filename: Optional[str] = None
self._dead = False
def __repr__(self):
@@ -559,8 +560,8 @@ class AbstractDownloadItem(QObject):
elif self.stats.percentage() is None:
return start
else:
- return utils.interpolate_color(start, stop,
- self.stats.percentage(), system)
+ return qtutils.interpolate_color(
+ start, stop, self.stats.percentage(), system)
def _do_cancel(self):
"""Actual cancel implementation."""
@@ -573,6 +574,7 @@ class AbstractDownloadItem(QObject):
Args:
remove_data: Whether to remove the downloaded data.
"""
+ assert not self.done
self._do_cancel()
log.downloads.debug("cancelled")
if remove_data:
@@ -877,7 +879,7 @@ class AbstractDownloadManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
- self.downloads = [] # type: typing.List[AbstractDownloadItem]
+ self.downloads: List[AbstractDownloadItem] = []
self._update_timer = usertypes.Timer(self, 'download-update')
self._update_timer.timeout.connect(self._update_gui)
self._update_timer.setInterval(_REFRESH_INTERVAL)
@@ -981,7 +983,8 @@ class AbstractDownloadManager(QObject):
def shutdown(self):
"""Cancel all downloads when shutting down."""
for download in self.downloads:
- download.cancel(remove_data=False)
+ if not download.done:
+ download.cancel(remove_data=False)
class DownloadModel(QAbstractListModel):
@@ -1251,7 +1254,7 @@ class DownloadModel(QAbstractListModel):
item = self[index.row()]
if role == Qt.DisplayRole:
- data = str(item) # type: typing.Any
+ data: Any = str(item)
elif role == Qt.ForegroundRole:
data = item.get_status_color('fg')
elif role == Qt.BackgroundRole:
@@ -1297,7 +1300,7 @@ class TempDownloadManager:
"""
def __init__(self):
- self.files = [] # type: typing.MutableSequence[typing.IO[bytes]]
+ self.files: MutableSequence[IO[bytes]] = []
self._tmpdir = None
def cleanup(self):
diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py
index 178fb5357..0bb5d4d08 100644
--- a/qutebrowser/browser/downloadview.py
+++ b/qutebrowser/browser/downloadview.py
@@ -20,44 +20,20 @@
"""The ListView to display downloads in."""
import functools
-import typing
+from typing import Callable, MutableSequence, Tuple, Union
-from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
+from PyQt5.QtCore import pyqtSlot, QSize, Qt
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.browser import downloads
from qutebrowser.config import stylesheet
from qutebrowser.utils import qtutils, utils
-from qutebrowser.qt import sip
-def update_geometry(obj):
- """Weird WORKAROUND for some weird PyQt bug (probably).
-
- This actually should be a method of DownloadView, but for some reason the
- rowsInserted/rowsRemoved signals don't get disconnected from this method
- when the DownloadView is deleted from Qt (e.g. by closing a window).
-
- Here we check if obj ("self") was deleted and just ignore the event if so.
-
- Original bug: https://github.com/qutebrowser/qutebrowser/issues/167
- Workaround bug: https://github.com/qutebrowser/qutebrowser/issues/171
- """
- def _update_geometry():
- """Actually update the geometry if the object still exists."""
- if sip.isdeleted(obj):
- return
- obj.updateGeometry()
-
- # If we don't use a singleShot QTimer, the geometry isn't updated correctly
- # and won't include the new item.
- QTimer.singleShot(0, _update_geometry)
-
-
-_ActionListType = typing.MutableSequence[
- typing.Union[
- typing.Tuple[None, None], # separator
- typing.Tuple[str, typing.Callable[[], None]],
+_ActionListType = MutableSequence[
+ Union[
+ Tuple[None, None], # separator
+ Tuple[str, Callable[[], None]],
]
]
@@ -93,9 +69,9 @@ class DownloadView(QListView):
self.setFlow(QListView.LeftToRight)
self.setSpacing(1)
self._menu = None
- model.rowsInserted.connect(functools.partial(update_geometry, self))
- model.rowsRemoved.connect(functools.partial(update_geometry, self))
- model.dataChanged.connect(functools.partial(update_geometry, self))
+ model.rowsInserted.connect(self._update_geometry)
+ model.rowsRemoved.connect(self._update_geometry)
+ model.dataChanged.connect(self._update_geometry)
self.setModel(model)
self.setWrapping(True)
self.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -110,6 +86,15 @@ class DownloadView(QListView):
count = model.rowCount()
return utils.get_repr(self, count=count)
+ @pyqtSlot()
+ def _update_geometry(self):
+ """Wrapper to call updateGeometry.
+
+ For some reason, this is needed so that PyQt disconnects the signals and handles
+ arguments correctly. Probably a WORKAROUND for an unknown PyQt bug.
+ """
+ self.updateGeometry()
+
@pyqtSlot(bool)
def on_fullscreen_requested(self, on):
"""Hide/show the downloadview when entering/leaving fullscreen."""
@@ -142,7 +127,7 @@ class DownloadView(QListView):
item: The DownloadItem to get the actions for, or None.
"""
model = self.model()
- actions = [] # type: _ActionListType
+ actions: _ActionListType = []
if item is None:
pass
elif item.done:
diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py
index 78ca67cd8..0195c73ad 100644
--- a/qutebrowser/browser/eventfilter.py
+++ b/qutebrowser/browser/eventfilter.py
@@ -22,48 +22,11 @@
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
from qutebrowser.config import config
-from qutebrowser.utils import message, log, usertypes, qtutils, objreg
+from qutebrowser.utils import message, log, usertypes, qtutils
from qutebrowser.misc import objects
from qutebrowser.keyinput import modeman
-class FocusWorkaroundEventFilter(QObject):
-
- """An event filter working Qt 5.11 keyboard focus issues.
-
- WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
- """
-
- def __init__(self, win_id, widget, parent=None):
- super().__init__(parent)
- self._win_id = win_id
- self._widget = widget
-
- def eventFilter(self, _obj, event):
- """Act on ChildAdded events."""
- if event.type() != QEvent.ChildAdded:
- return False
-
- pass_modes = [usertypes.KeyMode.command,
- usertypes.KeyMode.prompt,
- usertypes.KeyMode.yesno]
-
- if modeman.instance(self._win_id).mode in pass_modes:
- return False
-
- tabbed_browser = objreg.get('tabbed-browser', scope='window',
- window=self._win_id)
- current_index = tabbed_browser.widget.currentIndex()
- try:
- widget_index = tabbed_browser.widget.indexOf(self._widget.parent())
- except RuntimeError:
- widget_index = -1
- if current_index == widget_index:
- QTimer.singleShot(0, self._widget.setFocus)
-
- return False
-
-
class ChildEventFilter(QObject):
"""An event filter re-adding TabEventFilter on ChildEvent.
@@ -204,13 +167,6 @@ class TabEventFilter(QObject):
message.info("Zoom level: {}%".format(perc), replace=True)
self._tab.zoom.set_factor(factor)
return True
- elif (e.modifiers() & Qt.ShiftModifier and
- not qtutils.version_check('5.9', compiled=False)):
- if e.angleDelta().y() > 0:
- self._tab.scroller.left()
- else:
- self._tab.scroller.right()
- return True
return False
@@ -226,7 +182,6 @@ class TabEventFilter(QObject):
True if the event should be filtered, False otherwise.
"""
return (e.isAutoRepeat() and
- qtutils.version_check('5.10', compiled=False) and
not qtutils.version_check('5.14', compiled=False) and
objects.backend == usertypes.Backend.QtWebEngine)
diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py
index 89d720682..df8b2b0c2 100644
--- a/qutebrowser/browser/greasemonkey.py
+++ b/qutebrowser/browser/greasemonkey.py
@@ -26,25 +26,27 @@ import fnmatch
import functools
import glob
import textwrap
-import typing
+from typing import cast, List, Sequence
import attr
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
- javascript, urlmatch, version, usertypes,
- qtutils)
+ javascript, urlmatch, version, usertypes)
from qutebrowser.api import cmdutils
from qutebrowser.browser import downloads
from qutebrowser.misc import objects
-gm_manager = typing.cast('GreasemonkeyManager', None)
+gm_manager = cast('GreasemonkeyManager', None)
-def _scripts_dir():
+def _scripts_dirs():
"""Get the directory of the scripts."""
- return os.path.join(standarddir.data(), 'greasemonkey')
+ return [
+ os.path.join(standarddir.data(), 'greasemonkey'),
+ os.path.join(standarddir.config(), 'greasemonkey'),
+ ]
class GreasemonkeyScript:
@@ -54,10 +56,10 @@ class GreasemonkeyScript:
def __init__(self, properties, code, # noqa: C901 pragma: no mccabe
filename=None):
self._code = code
- self.includes = [] # type: typing.Sequence[str]
- self.matches = [] # type: typing.Sequence[str]
- self.excludes = [] # type: typing.Sequence[str]
- self.requires = [] # type: typing.Sequence[str]
+ self.includes: Sequence[str] = []
+ self.matches: Sequence[str] = []
+ self.excludes: Sequence[str] = []
+ self.requires: Sequence[str] = []
self.description = None
self.namespace = None
self.run_at = None
@@ -125,8 +127,7 @@ class GreasemonkeyScript:
def needs_document_end_workaround(self):
"""Check whether to force @run-at document-end.
- This needs to be done on QtWebEngine with Qt 5.12 for known-broken
- scripts.
+ This needs to be done on QtWebEngine (since Qt 5.12) for known-broken scripts.
On Qt 5.12, accessing the DOM isn't possible with "@run-at
document-start". It was documented to be impossible before, but seems
@@ -135,12 +136,11 @@ class GreasemonkeyScript:
However, some scripts do DOM access with "@run-at document-start". Fix
those by forcing them to use document-end instead.
"""
- if objects.backend != usertypes.Backend.QtWebEngine:
- assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
- return False
- elif not qtutils.version_check('5.12', compiled=False):
+ if objects.backend == usertypes.Backend.QtWebKit:
return False
+ assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
+
broken_scripts = [
('http://userstyles.org', None),
('https://github.com/ParticleCore', 'Iridium'),
@@ -259,11 +259,10 @@ class GreasemonkeyManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
- self._run_start = [] # type: typing.List[GreasemonkeyScript]
- self._run_end = [] # type: typing.List[GreasemonkeyScript]
- self._run_idle = [] # type: typing.List[GreasemonkeyScript]
- self._in_progress_dls = [
- ] # type: typing.List[downloads.AbstractDownloadItem]
+ self._run_start: List[GreasemonkeyScript] = []
+ self._run_end: List[GreasemonkeyScript] = []
+ self._run_idle: List[GreasemonkeyScript] = []
+ self._in_progress_dls: List[downloads.AbstractDownloadItem] = []
self.load_scripts()
@@ -281,18 +280,19 @@ class GreasemonkeyManager(QObject):
self._run_end = []
self._run_idle = []
- scripts_dir = os.path.abspath(_scripts_dir())
- log.greasemonkey.debug("Reading scripts from: {}".format(scripts_dir))
- for script_filename in glob.glob(os.path.join(scripts_dir, '*.js')):
- if not os.path.isfile(script_filename):
- continue
- script_path = os.path.join(scripts_dir, script_filename)
- with open(script_path, encoding='utf-8-sig') as script_file:
- script = GreasemonkeyScript.parse(script_file.read(),
- script_filename)
- if not script.name:
- script.name = script_filename
- self.add_script(script, force)
+ for scripts_dir in _scripts_dirs():
+ scripts_dir = os.path.abspath(scripts_dir)
+ log.greasemonkey.debug("Reading scripts from: {}".format(scripts_dir))
+ for script_filename in glob.glob(os.path.join(scripts_dir, '*.js')):
+ if not os.path.isfile(script_filename):
+ continue
+ script_path = os.path.join(scripts_dir, script_filename)
+ with open(script_path, encoding='utf-8-sig') as script_file:
+ script = GreasemonkeyScript.parse(script_file.read(),
+ script_filename)
+ if not script.name:
+ script.name = script_filename
+ self.add_script(script, force)
self.scripts_reloaded.emit()
def add_script(self, script, force=False):
@@ -329,7 +329,7 @@ class GreasemonkeyManager(QObject):
log.greasemonkey.debug("Loaded script: {}".format(script.name))
def _required_url_to_file_path(self, url):
- requires_dir = os.path.join(_scripts_dir(), 'requires')
+ requires_dir = os.path.join(_scripts_dirs()[0], 'requires')
if not os.path.exists(requires_dir):
os.mkdir(requires_dir)
return os.path.join(requires_dir, utils.sanitize_filename(url))
@@ -430,7 +430,7 @@ def greasemonkey_reload(force=False):
"""Re-read Greasemonkey scripts from disk.
The scripts are read from a 'greasemonkey' subdirectory in
- qutebrowser's data directory (see `:version`).
+ qutebrowser's data or config directories (see `:version`).
Args:
force: For any scripts that have required dependencies,
@@ -444,7 +444,8 @@ def init():
global gm_manager
gm_manager = GreasemonkeyManager()
- try:
- os.mkdir(_scripts_dir())
- except FileExistsError:
- pass
+ for scripts_dir in _scripts_dirs():
+ try:
+ os.mkdir(scripts_dir)
+ except FileExistsError:
+ pass
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index daf38a755..f914f3085 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -20,16 +20,17 @@
"""A HintManager to draw hints over links."""
import collections
-import typing
import functools
import os
import re
import html
import enum
from string import ascii_lowercase
+from typing import (TYPE_CHECKING, Callable, Dict, Iterable, Iterator, List, Mapping,
+ MutableSequence, Optional, Sequence, Set)
import attr
-from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, configexc
@@ -38,14 +39,30 @@ from qutebrowser.browser import webelem, history
from qutebrowser.commands import userscripts, runners
from qutebrowser.api import cmdutils
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser import browsertab
-Target = enum.Enum('Target', ['normal', 'current', 'tab', 'tab_fg', 'tab_bg',
- 'window', 'yank', 'yank_primary', 'run', 'fill',
- 'hover', 'download', 'userscript', 'spawn',
- 'delete', 'right_click'])
+class Target(enum.Enum):
+
+ """What action to take on a hint."""
+
+ normal = enum.auto()
+ current = enum.auto()
+ tab = enum.auto()
+ tab_fg = enum.auto()
+ tab_bg = enum.auto()
+ window = enum.auto()
+ yank = enum.auto()
+ yank_primary = enum.auto()
+ run = enum.auto()
+ fill = enum.auto()
+ hover = enum.auto()
+ download = enum.auto()
+ userscript = enum.auto()
+ spawn = enum.auto()
+ delete = enum.auto()
+ right_click = enum.auto()
class HintingError(Exception):
@@ -164,22 +181,22 @@ class HintContext:
group: The group of web elements to hint.
"""
- all_labels = attr.ib(attr.Factory(list)) # type: typing.List[HintLabel]
- labels = attr.ib(attr.Factory(dict)) # type: typing.Dict[str, HintLabel]
- target = attr.ib(None) # type: Target
- baseurl = attr.ib(None) # type: QUrl
- to_follow = attr.ib(None) # type: str
- rapid = attr.ib(False) # type: bool
- first_run = attr.ib(True) # type: bool
- add_history = attr.ib(False) # type: bool
- filterstr = attr.ib(None) # type: str
- args = attr.ib(attr.Factory(list)) # type: typing.List[str]
- tab = attr.ib(None) # type: browsertab.AbstractTab
- group = attr.ib(None) # type: str
- hint_mode = attr.ib(None) # type: str
- first = attr.ib(False) # type: bool
-
- def get_args(self, urlstr: str) -> typing.Sequence[str]:
+ all_labels: List[HintLabel] = attr.ib(attr.Factory(list))
+ labels: Dict[str, HintLabel] = attr.ib(attr.Factory(dict))
+ target: Target = attr.ib(None)
+ baseurl: QUrl = attr.ib(None)
+ to_follow: str = attr.ib(None)
+ rapid: bool = attr.ib(False)
+ first_run: bool = attr.ib(True)
+ add_history: bool = attr.ib(False)
+ filterstr: str = attr.ib(None)
+ args: List[str] = attr.ib(attr.Factory(list))
+ tab: 'browsertab.AbstractTab' = attr.ib(None)
+ group: str = attr.ib(None)
+ hint_mode: str = attr.ib(None)
+ first: bool = attr.ib(False)
+
+ def get_args(self, urlstr: str) -> Sequence[str]:
"""Get the arguments, with {hint-url} replaced by the given URL."""
args = []
for arg in self.args:
@@ -336,8 +353,8 @@ class HintActions:
commandrunner.run_safely('spawn ' + ' '.join(args))
-_ElemsType = typing.Sequence[webelem.AbstractWebElement]
-_HintStringsType = typing.MutableSequence[str]
+_ElemsType = Sequence[webelem.AbstractWebElement]
+_HintStringsType = MutableSequence[str]
class HintManager(QObject):
@@ -353,7 +370,7 @@ class HintManager(QObject):
_tab_id: The tab ID this HintManager is associated with.
Signals:
- See HintActions
+ set_text: Request for the statusbar to change its text.
"""
HINT_TEXTS = {
@@ -375,11 +392,13 @@ class HintManager(QObject):
Target.delete: "Delete an element",
}
+ set_text = pyqtSignal(str)
+
def __init__(self, win_id: int, parent: QObject = None) -> None:
"""Constructor."""
super().__init__(parent)
self._win_id = win_id
- self._context = None # type: typing.Optional[HintContext]
+ self._context: Optional[HintContext] = None
self._word_hinter = WordHinter()
self._actions = HintActions(win_id)
@@ -402,10 +421,8 @@ class HintManager(QObject):
for label in self._context.all_labels:
label.cleanup()
- text = self._get_text()
- message_bridge = objreg.get('message-bridge', scope='window',
- window=self._win_id)
- message_bridge.maybe_reset_text(text)
+ self.set_text.emit('')
+
self._context = None
def _hint_strings(self, elems: _ElemsType) -> _HintStringsType:
@@ -511,12 +528,10 @@ class HintManager(QObject):
Return:
A list of shuffled hint strings.
"""
- buckets = [
- [] for i in range(length)
- ] # type: typing.Sequence[_HintStringsType]
+ buckets: Sequence[_HintStringsType] = [[] for i in range(length)]
for i, hint in enumerate(hints):
buckets[i % len(buckets)].append(hint)
- result = [] # type: _HintStringsType
+ result: _HintStringsType = []
for bucket in buckets:
result += bucket
return result
@@ -541,7 +556,7 @@ class HintManager(QObject):
A hint string.
"""
base = len(chars)
- hintstr = [] # type: typing.MutableSequence[str]
+ hintstr: MutableSequence[str] = []
remainder = 0
while True:
remainder = number % base
@@ -636,9 +651,7 @@ class HintManager(QObject):
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
- message_bridge = objreg.get('message-bridge', scope='window',
- window=self._win_id)
- message_bridge.set_text(self._get_text())
+ self.set_text.emit(self._get_text())
if self._context.first:
self._fire(strings[0])
@@ -771,7 +784,7 @@ class HintManager(QObject):
error_cb=lambda err: message.error(str(err)),
only_visible=True)
- def _get_hint_mode(self, mode: typing.Optional[str]) -> str:
+ def _get_hint_mode(self, mode: Optional[str]) -> str:
"""Get the hinting mode to use based on a mode argument."""
if mode is None:
return config.val.hints.mode
@@ -783,7 +796,7 @@ class HintManager(QObject):
raise cmdutils.CommandError("Invalid mode: {}".format(e))
return mode
- def current_mode(self) -> typing.Optional[str]:
+ def current_mode(self) -> Optional[str]:
"""Return the currently active hinting mode (or None otherwise)."""
if self._context is None:
return None
@@ -794,7 +807,7 @@ class HintManager(QObject):
self,
keystr: str = "",
filterstr: str = "",
- visible: typing.Mapping[str, HintLabel] = None
+ visible: Mapping[str, HintLabel] = None
) -> None:
"""Handle the auto_follow option."""
assert self._context is not None
@@ -856,7 +869,7 @@ class HintManager(QObject):
pass
self._handle_auto_follow(keystr=keystr)
- def filter_hints(self, filterstr: typing.Optional[str]) -> None:
+ def filter_hints(self, filterstr: Optional[str]) -> None:
"""Filter displayed hints according to a text.
Args:
@@ -1027,7 +1040,7 @@ class WordHinter:
def __init__(self) -> None:
# will be initialized on first use.
- self.words = set() # type: typing.Set[str]
+ self.words: Set[str] = set()
self.dictionary = None
def ensure_initialized(self) -> None:
@@ -1059,10 +1072,10 @@ class WordHinter:
def extract_tag_words(
self, elem: webelem.AbstractWebElement
- ) -> typing.Iterator[str]:
+ ) -> Iterator[str]:
"""Extract tag words form the given element."""
- _extractor_type = typing.Callable[[webelem.AbstractWebElement], str]
- attr_extractors = {
+ _extractor_type = Callable[[webelem.AbstractWebElement], str]
+ attr_extractors: Mapping[str, _extractor_type] = {
"alt": lambda elem: elem["alt"],
"name": lambda elem: elem["name"],
"title": lambda elem: elem["title"],
@@ -1070,7 +1083,7 @@ class WordHinter:
"src": lambda elem: elem["src"].split('/')[-1],
"href": lambda elem: elem["href"].split('/')[-1],
"text": str,
- } # type: typing.Mapping[str, _extractor_type]
+ }
extractable_attrs = collections.defaultdict(list, {
"img": ["alt", "title", "src"],
@@ -1086,8 +1099,8 @@ class WordHinter:
def tag_words_to_hints(
self,
- words: typing.Iterable[str]
- ) -> typing.Iterator[str]:
+ words: Iterable[str]
+ ) -> Iterator[str]:
"""Take words and transform them to proper hints if possible."""
for candidate in words:
if not candidate:
@@ -1098,20 +1111,20 @@ class WordHinter:
if 4 < match.end() - match.start() < 8:
yield candidate[match.start():match.end()].lower()
- def any_prefix(self, hint: str, existing: typing.Iterable[str]) -> bool:
+ def any_prefix(self, hint: str, existing: Iterable[str]) -> bool:
return any(hint.startswith(e) or e.startswith(hint) for e in existing)
def filter_prefixes(
self,
- hints: typing.Iterable[str],
- existing: typing.Iterable[str]
- ) -> typing.Iterator[str]:
+ hints: Iterable[str],
+ existing: Iterable[str]
+ ) -> Iterator[str]:
"""Filter hints which don't start with the given prefix."""
return (h for h in hints if not self.any_prefix(h, existing))
def new_hint_for(self, elem: webelem.AbstractWebElement,
- existing: typing.Iterable[str],
- fallback: typing.Iterable[str]) -> typing.Optional[str]:
+ existing: Iterable[str],
+ fallback: Iterable[str]) -> Optional[str]:
"""Return a hint for elem, not conflicting with the existing."""
new = self.tag_words_to_hints(self.extract_tag_words(elem))
new_no_prefixes = self.filter_prefixes(new, existing)
@@ -1135,7 +1148,7 @@ class WordHinter:
"""
self.ensure_initialized()
hints = []
- used_hints = set() # type: typing.Set[str]
+ used_hints: Set[str] = set()
words = iter(self.words)
for elem in elems:
hint = self.new_hint_for(elem, used_hints, words)
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
index b7221dc15..89061cebf 100644
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -22,7 +22,7 @@
import os
import time
import contextlib
-import typing
+from typing import cast, Mapping, MutableSequence
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal
from PyQt5.QtWidgets import QProgressDialog, QApplication
@@ -35,7 +35,7 @@ from qutebrowser.misc import objects, sql
# increment to indicate that HistoryCompletion must be regenerated
_USER_VERSION = 2
-web_history = typing.cast('WebHistory', None)
+web_history = cast('WebHistory', None)
class HistoryProgress:
@@ -208,11 +208,11 @@ class WebHistory(sql.SqlTable):
return any(pattern.matches(url) for pattern in patterns)
def _rebuild_completion(self):
- data = {
+ data: Mapping[str, MutableSequence[str]] = {
'url': [],
'title': [],
'last_atime': []
- } # type: typing.Mapping[str, typing.MutableSequence[str]]
+ }
# select the latest entry for each url
q = sql.Query('SELECT url, title, max(atime) AS atime FROM History '
'WHERE NOT redirect and url NOT LIKE "qute://back%" '
diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py
index 390762ae0..813f1ff12 100644
--- a/qutebrowser/browser/inspector.py
+++ b/qutebrowser/browser/inspector.py
@@ -21,8 +21,8 @@
import base64
import binascii
-import typing
import enum
+from typing import cast, Optional
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent
@@ -49,12 +49,7 @@ def create(*, splitter: 'miscwidgets.InspectorSplitter',
# argument and to avoid circular imports.
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webengineinspector
- if webengineinspector.supports_new():
- return webengineinspector.WebEngineInspector(
- splitter, win_id, parent)
- else:
- return webengineinspector.LegacyWebEngineInspector(
- splitter, win_id, parent)
+ return webengineinspector.WebEngineInspector(splitter, win_id, parent)
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkitinspector
return webkitinspector.WebKitInspector(splitter, win_id, parent)
@@ -65,11 +60,11 @@ class Position(enum.Enum):
"""Where the inspector is shown."""
- right = 1
- left = 2
- top = 3
- bottom = 4
- window = 5
+ right = enum.auto()
+ left = enum.auto()
+ top = enum.auto()
+ bottom = enum.auto()
+ window = enum.auto()
class Error(Exception):
@@ -119,10 +114,10 @@ class AbstractWebInspector(QWidget):
win_id: int,
parent: QWidget = None) -> None:
super().__init__(parent)
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._layout = miscwidgets.WrapperLayout(self)
self._splitter = splitter
- self._position = None # type: typing.Optional[Position]
+ self._position: Optional[Position] = None
self._win_id = win_id
self._event_filter = _EventFilter(parent=self)
@@ -163,7 +158,7 @@ class AbstractWebInspector(QWidget):
modeman.enter(self._win_id, usertypes.KeyMode.insert,
reason='Inspector clicked', only_if_normal=True)
- def set_position(self, position: typing.Optional[Position]) -> None:
+ def set_position(self, position: Optional[Position]) -> None:
"""Set the position of the inspector.
If the position is None, the last known position is used.
diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py
index 194246344..bace6fa6a 100644
--- a/qutebrowser/browser/navigate.py
+++ b/qutebrowser/browser/navigate.py
@@ -21,7 +21,7 @@
import re
import posixpath
-import typing
+from typing import Optional, Set
from PyQt5.QtCore import QUrl
@@ -97,9 +97,9 @@ def incdec(url, count, inc_or_dec):
window: Open the link in a new window.
"""
urlutils.ensure_valid(url)
- segments = (
+ segments: Optional[Set[str]] = (
set(config.val.url.incdec_segments)
- ) # type: typing.Optional[typing.Set[str]]
+ )
if segments is None:
segments = {'path', 'query'}
@@ -134,13 +134,13 @@ def path_up(url, count):
"""
urlutils.ensure_valid(url)
url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
- path = url.path()
+ path = url.path(QUrl.FullyEncoded)
if not path or path == '/':
raise Error("Can't go up!")
for _i in range(0, min(count, path.count('/'))):
path = posixpath.join(path, posixpath.pardir)
path = posixpath.normpath(path)
- url.setPath(path)
+ url.setPath(path, QUrl.StrictMode)
return url
@@ -154,14 +154,19 @@ def strip(url, count):
def _find_prevnext(prev, elems):
"""Find a prev/next element in the given list of elements."""
- # First check for <link rel="prev(ious)|next">
+ # First check for <link rel="prev(ious)|next"> as well as
+ # e.g. <a class="nav-(prev|next)"> (Hugo)
rel_values = {'prev', 'previous'} if prev else {'next'}
+ classes = {'nav-prev'} if prev else {'nav-next'}
for e in elems:
- if e.tag_name() not in ['link', 'a'] or 'rel' not in e:
+ if e.tag_name() not in ['link', 'a']:
continue
- if set(e['rel'].split(' ')) & rel_values:
+ if 'rel' in e and set(e['rel'].split(' ')) & rel_values:
log.hints.debug("Found {!r} with rel={}".format(e, e['rel']))
return e
+ elif e.classes() & classes:
+ log.hints.debug("Found {!r} with class={}".format(e, e.classes()))
+ return e
# Then check for regular links/buttons.
elems = [e for e in elems if e.tag_name() != 'link']
diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py
index 6ae01c7d8..3b5686a03 100644
--- a/qutebrowser/browser/network/pac.py
+++ b/qutebrowser/browser/network/pac.py
@@ -21,7 +21,7 @@
import sys
import functools
-import typing
+from typing import Optional
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo,
@@ -66,7 +66,7 @@ def _js_slot(*args):
return self._error_con.callAsConstructor([e])
# pylint: enable=protected-access
- deco = pyqtSlot(*args, result=QJSValue) # type: ignore[arg-type]
+ deco = pyqtSlot(*args, result=QJSValue)
return deco(new_method)
return _decorator
@@ -251,8 +251,7 @@ class PACFetcher(QObject):
url.setScheme(url.scheme()[len(pac_prefix):])
self._pac_url = url
- self._manager = QNetworkAccessManager(
- ) # type: typing.Optional[QNetworkAccessManager]
+ self._manager: Optional[QNetworkAccessManager] = QNetworkAccessManager()
self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
self._pac = None
self._error_message = None
diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py
index 0c6d02501..f60061045 100644
--- a/qutebrowser/browser/pdfjs.py
+++ b/qutebrowser/browser/pdfjs.py
@@ -24,9 +24,7 @@ import os
from PyQt5.QtCore import QUrl, QUrlQuery
-from qutebrowser.utils import (utils, javascript, jinja, qtutils, usertypes,
- standarddir, log)
-from qutebrowser.misc import objects
+from qutebrowser.utils import utils, javascript, jinja, standarddir, log
from qutebrowser.config import config
@@ -104,29 +102,17 @@ def _generate_pdfjs_script(filename):
document.addEventListener("DOMContentLoaded", function() {
if (typeof window.PDFJS !== 'undefined') {
// v1.x
- {% if disable_create_object_url %}
- window.PDFJS.disableCreateObjectURL = true;
- {% endif %}
window.PDFJS.verbosity = window.PDFJS.VERBOSITY_LEVELS.info;
} else {
// v2.x
const options = window.PDFViewerApplicationOptions;
- {% if disable_create_object_url %}
- options.set('disableCreateObjectURL', true);
- {% endif %}
options.set('verbosity', pdfjsLib.VerbosityLevel.INFOS);
}
const viewer = window.PDFView || window.PDFViewerApplication;
viewer.open({{ url }});
});
- """).render(
- url=js_url,
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70420
- disable_create_object_url=(
- not qtutils.version_check('5.12') and
- not qtutils.version_check('5.7.1', exact=True, compiled=False) and
- objects.backend == usertypes.Backend.QtWebEngine))
+ """).render(url=js_url)
def get_pdfjs_res_and_path(path):
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index 0bafeeaf9..0bf165965 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -23,7 +23,7 @@ import io
import os.path
import shutil
import functools
-import typing
+from typing import Dict, IO, Optional
import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl
@@ -92,8 +92,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
reply: The QNetworkReply to download.
"""
super().__init__(manager=manager, parent=manager)
- self.fileobj = None # type: typing.Optional[typing.IO[bytes]]
- self.raw_headers = {} # type: typing.Dict[bytes, bytes]
+ self.fileobj: Optional[IO[bytes]] = None
+ self.raw_headers: Dict[bytes, bytes] = {}
self._autoclose = True
self._retry_info = None
@@ -181,11 +181,16 @@ class DownloadItem(downloads.AbstractDownloadItem):
assert self.done
assert not self.successful
assert self._retry_info is not None
+
+ # Not calling self.cancel() here because the download is done (albeit
+ # unsuccessfully)
+ self.remove()
+ self.delete()
+
new_reply = self._retry_info.manager.get(self._retry_info.request)
new_download = self._manager.fetch(new_reply,
suggested_filename=self.basename)
self.adopt_download.emit(new_download)
- self.cancel()
def _get_open_filename(self):
filename = self._filename
@@ -414,11 +419,12 @@ class DownloadManager(downloads.AbstractDownloadManager):
private=config.val.content.private_browsing, parent=self)
@pyqtSlot('QUrl')
- def get(self, url, **kwargs):
+ def get(self, url, cache=True, **kwargs):
"""Start a download with a link URL.
Args:
url: The URL to get, as QUrl
+ cache: If set to False, don't cache the response.
**kwargs: passed to get_request().
Return:
@@ -432,6 +438,9 @@ class DownloadManager(downloads.AbstractDownloadManager):
user_agent = websettings.user_agent(url)
req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)
+ if not cache:
+ req.setAttribute(QNetworkRequest.CacheSaveControlAttribute, False)
+
return self.get_request(req, **kwargs)
def get_mhtml(self, tab, target):
diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py
index b661f533d..cb4a9ba61 100644
--- a/qutebrowser/browser/qutescheme.py
+++ b/qutebrowser/browser/qutescheme.py
@@ -31,23 +31,16 @@ import time
import textwrap
import urllib
import collections
-import base64
-import typing
-from typing import TypeVar, Callable, Union, Tuple
+import secrets
+from typing import TypeVar, Callable, Dict, List, Optional, Union, Sequence, Tuple
-try:
- import secrets
-except ImportError:
- # New in Python 3.6
- secrets = None # type: ignore[assignment]
-
-from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
+from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser
from qutebrowser.browser import pdfjs, downloads, history
-from qutebrowser.config import config, configdata, configexc, configdiff
+from qutebrowser.config import config, configdata, configexc
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
- objreg, urlutils, standarddir)
+ objreg, standarddir)
from qutebrowser.qt import sip
@@ -112,7 +105,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
def __init__(self, name):
self._name = name
- self._function = None # type: typing.Optional[typing.Callable]
+ self._function: Optional[Callable] = None
def __call__(self, function: _Handler) -> _Handler:
self._function = function
@@ -125,7 +118,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
return self._function(*args, **kwargs)
-def data_for_url(url: QUrl) -> typing.Tuple[str, bytes]:
+def data_for_url(url: QUrl) -> Tuple[str, bytes]:
"""Get the data to show for the given URL.
Args:
@@ -142,7 +135,7 @@ def data_for_url(url: QUrl) -> typing.Tuple[str, bytes]:
path = url.path()
host = url.host()
- query = urlutils.query_string(url)
+ query = url.query()
# A url like "qute:foo" is split as "scheme:path", not "scheme:host".
log.misc.debug("url: {}, path: {}, host {}".format(
url.toDisplayString(), path, host))
@@ -199,8 +192,7 @@ def qute_bookmarks(_url: QUrl) -> _HandlerRet:
@add_handler('tabs')
def qute_tabs(_url: QUrl) -> _HandlerRet:
"""Handler for qute://tabs. Display information about all open tabs."""
- tabs = collections.defaultdict(
- list) # type: typing.Dict[str, typing.List[typing.Tuple[str, str]]]
+ tabs: Dict[str, List[Tuple[str, str]]] = collections.defaultdict(list)
for win_id, window in objreg.window_registry.items():
if sip.isdeleted(window):
continue
@@ -221,7 +213,7 @@ def qute_tabs(_url: QUrl) -> _HandlerRet:
def history_data(
start_time: float,
offset: int = None
-) -> typing.Sequence[typing.Dict[str, typing.Union[str, int]]]:
+) -> Sequence[Dict[str, Union[str, int]]]:
"""Return history data.
Arguments:
@@ -355,7 +347,7 @@ def qute_gpl(_url: QUrl) -> _HandlerRet:
return 'text/html', utils.read_file('html/license.html')
-def _asciidoc_fallback_path(html_path: str) -> typing.Optional[str]:
+def _asciidoc_fallback_path(html_path: str) -> Optional[str]:
"""Fall back to plaintext asciidoc if the HTML is unavailable."""
path = html_path.replace('.html', '.asciidoc')
try:
@@ -449,12 +441,7 @@ def qute_settings(url: QUrl) -> _HandlerRet:
# Requests to qute://settings/set should only be allowed from
# qute://settings. As an additional security precaution, we generate a CSRF
# token to use here.
- if secrets:
- csrf_token = secrets.token_urlsafe()
- else:
- # On Python < 3.6, from secrets.py
- token = base64.urlsafe_b64encode(os.urandom(32))
- csrf_token = token.rstrip(b'=').decode('ascii')
+ csrf_token = secrets.token_urlsafe()
src = jinja.render('settings.html', title='settings',
configdata=configdata,
@@ -494,18 +481,10 @@ def qute_back(url: QUrl) -> _HandlerRet:
@add_handler('configdiff')
-def qute_configdiff(url: QUrl) -> _HandlerRet:
+def qute_configdiff(_url: QUrl) -> _HandlerRet:
"""Handler for qute://configdiff."""
- if url.path() == '/old':
- try:
- return 'text/html', configdiff.get_diff()
- except OSError as e:
- error = (b'Failed to read old config: ' +
- str(e.strerror).encode('utf-8'))
- return 'text/plain', error
- else:
- data = config.instance.dump_userconfig().encode('utf-8')
- return 'text/plain', data
+ data = config.instance.dump_userconfig().encode('utf-8')
+ return 'text/plain', data
@add_handler('pastebin-version')
@@ -576,11 +555,7 @@ def qute_pdfjs(url: QUrl) -> _HandlerRet:
def qute_warning(url: QUrl) -> _HandlerRet:
"""Handler for qute://warning."""
path = url.path()
- if path == '/old-qt':
- src = jinja.render('warning-old-qt.html',
- title='Old Qt warning',
- qt_version=qVersion())
- elif path == '/webkit':
+ if path == '/webkit':
src = jinja.render('warning-webkit.html',
title='QtWebKit backend warning')
elif path == '/sessions':
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 715487def..9234e82d8 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -22,13 +22,12 @@
import os
import html
import netrc
-import typing
+from typing import Callable, Mapping
from PyQt5.QtCore import QUrl
from qutebrowser.config import config
-from qutebrowser.utils import (usertypes, message, log, objreg, jinja, utils,
- qtutils)
+from qutebrowser.utils import usertypes, message, log, objreg, jinja, utils
from qutebrowser.mainwindow import mainwindow
@@ -76,15 +75,14 @@ def authentication_required(url, authenticator, abort_on):
return answer
-def javascript_confirm(url, js_msg, abort_on, *, escape_msg=True):
+def javascript_confirm(url, js_msg, abort_on):
"""Display a javascript confirm prompt."""
log.js.debug("confirm: {}".format(js_msg))
if config.val.content.javascript.modal_dialog:
raise CallSuper
- js_msg = html.escape(js_msg) if escape_msg else js_msg
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
- js_msg)
+ html.escape(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
ans = message.ask('Javascript confirm', msg,
mode=usertypes.PromptMode.yesno,
@@ -92,7 +90,7 @@ def javascript_confirm(url, js_msg, abort_on, *, escape_msg=True):
return bool(ans)
-def javascript_prompt(url, js_msg, default, abort_on, *, escape_msg=True):
+def javascript_prompt(url, js_msg, default, abort_on):
"""Display a javascript prompt."""
log.js.debug("prompt: {}".format(js_msg))
if config.val.content.javascript.modal_dialog:
@@ -100,9 +98,8 @@ def javascript_prompt(url, js_msg, default, abort_on, *, escape_msg=True):
if not config.val.content.javascript.prompt:
return (False, "")
- js_msg = html.escape(js_msg) if escape_msg else js_msg
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
- js_msg)
+ html.escape(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
answer = message.ask('Javascript prompt', msg,
mode=usertypes.PromptMode.text,
@@ -115,7 +112,7 @@ def javascript_prompt(url, js_msg, default, abort_on, *, escape_msg=True):
return (True, answer)
-def javascript_alert(url, js_msg, abort_on, *, escape_msg=True):
+def javascript_alert(url, js_msg, abort_on):
"""Display a javascript alert."""
log.js.debug("alert: {}".format(js_msg))
if config.val.content.javascript.modal_dialog:
@@ -124,9 +121,8 @@ def javascript_alert(url, js_msg, abort_on, *, escape_msg=True):
if not config.val.content.javascript.alert:
return
- js_msg = html.escape(js_msg) if escape_msg else js_msg
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
- js_msg)
+ html.escape(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
abort_on=abort_on, url=urlstr)
@@ -134,13 +130,13 @@ def javascript_alert(url, js_msg, abort_on, *, escape_msg=True):
# Needs to line up with the values allowed for the
# content.javascript.log setting.
-_JS_LOGMAP = {
+_JS_LOGMAP: Mapping[str, Callable[[str], None]] = {
'none': lambda arg: None,
'debug': log.js.debug,
'info': log.js.info,
'warning': log.js.warning,
'error': log.js.error,
-} # type: typing.Mapping[str, typing.Callable[[str], None]]
+}
def javascript_log_message(level, source, line, msg):
@@ -287,9 +283,7 @@ def get_user_stylesheet(searching=False):
css += f.read()
setting = config.val.scrolling.bar
- overlay_bar_available = (qtutils.version_check('5.11', compiled=False) and
- not utils.is_mac)
- if setting == 'overlay' and not overlay_bar_available:
+ if setting == 'overlay' and not utils.is_mac:
setting = 'when-searching'
if setting == 'never' or setting == 'when-searching' and not searching:
diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py
index b70deb165..348a7a2ff 100644
--- a/qutebrowser/browser/signalfilter.py
+++ b/qutebrowser/browser/signalfilter.py
@@ -50,7 +50,7 @@ class SignalFilter(QObject):
"""Factory for partial _filter_signals functions.
Args:
- signal: The pyqtSignal to filter.
+ signal: The pyqtBoundSignal to filter.
tab: The WebView to create filters for.
Return:
diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py
index 18fd15771..46d9d450d 100644
--- a/qutebrowser/browser/urlmarks.py
+++ b/qutebrowser/browser/urlmarks.py
@@ -30,7 +30,7 @@ import os.path
import html
import functools
import collections
-import typing
+from typing import MutableMapping
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
@@ -78,8 +78,7 @@ class UrlMarkManager(QObject):
"""Initialize and read quickmarks."""
super().__init__(parent)
- self.marks = collections.OrderedDict(
- ) # type: typing.MutableMapping[str, str]
+ self.marks: MutableMapping[str, str] = collections.OrderedDict()
self._init_lineparser()
for line in self._lineparser:
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index 98c5bd6d1..7a888daeb 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -19,7 +19,7 @@
"""Generic web element related code."""
-import typing
+from typing import cast, TYPE_CHECKING, Iterator, Optional, Set, Union
import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint
@@ -29,11 +29,11 @@ from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser import browsertab
-JsValueType = typing.Union[int, float, str, None]
+JsValueType = Union[int, float, str, None]
class Error(Exception):
@@ -80,7 +80,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
def __delitem__(self, key: str) -> None:
raise NotImplementedError
- def __iter__(self) -> typing.Iterator[str]:
+ def __iter__(self) -> Iterator[str]:
raise NotImplementedError
def __len__(self) -> int:
@@ -88,8 +88,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
def __repr__(self) -> str:
try:
- html = utils.compact_text(
- self.outer_xml(), 500) # type: typing.Optional[str]
+ html: Optional[str] = utils.compact_text(self.outer_xml(), 500)
except Error:
html = None
return utils.get_repr(self, html=html)
@@ -102,8 +101,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Get the geometry for this element."""
raise NotImplementedError
- def classes(self) -> typing.List[str]:
- """Get a list of classes assigned to this element."""
+ def classes(self) -> Set[str]:
+ """Get a set of classes assigned to this element."""
raise NotImplementedError
def tag_name(self) -> str:
@@ -282,7 +281,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Remove target from link."""
raise NotImplementedError
- def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]:
+ def resolve_url(self, baseurl: QUrl) -> Optional[QUrl]:
"""Resolve the URL in the element's src/href attribute.
Args:
@@ -357,16 +356,12 @@ class AbstractWebElement(collections.abc.MutableMapping):
else:
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
- modifiers = typing.cast(Qt.KeyboardModifiers,
- target_modifiers[click_target])
+ modifiers = cast(Qt.KeyboardModifiers, target_modifiers[click_target])
events = [
- QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
- Qt.NoModifier),
- QMouseEvent(QEvent.MouseButtonPress, pos, button, button,
- modifiers),
- QMouseEvent(QEvent.MouseButtonRelease, pos, button, Qt.NoButton,
- modifiers),
+ QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, Qt.NoModifier),
+ QMouseEvent(QEvent.MouseButtonPress, pos, button, button, modifiers),
+ QMouseEvent(QEvent.MouseButtonRelease, pos, button, Qt.NoButton, modifiers),
]
for evt in events:
diff --git a/qutebrowser/browser/webengine/cookies.py b/qutebrowser/browser/webengine/cookies.py
index 697082830..a5d92ef74 100644
--- a/qutebrowser/browser/webengine/cookies.py
+++ b/qutebrowser/browser/webengine/cookies.py
@@ -20,7 +20,7 @@
"""Filter for QtWebEngine cookies."""
from qutebrowser.config import config
-from qutebrowser.utils import utils, qtutils, log
+from qutebrowser.utils import utils, log
from qutebrowser.misc import objects
@@ -31,13 +31,6 @@ def _accept_cookie(request):
if not url.isValid():
url = None
- if qtutils.version_check('5.11.3', compiled=False):
- third_party = request.thirdParty
- else:
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-71393
- third_party = (request.thirdParty and
- not request.firstPartyUrl.isEmpty())
-
accept = config.instance.get('content.cookies.accept',
url=url)
@@ -48,13 +41,13 @@ def _accept_cookie(request):
else request.origin.toDisplayString())
log.network.debug('Cookie from origin {} on {} (third party: {}) '
'-> applying setting {}'
- .format(origin_str, first_party_str, third_party,
+ .format(origin_str, first_party_str, request.thirdParty,
accept))
if accept == 'all':
return True
elif accept in ['no-3rdparty', 'no-unknown-3rdparty']:
- return not third_party
+ return not request.thirdParty
elif accept == 'never':
return False
else:
@@ -62,12 +55,5 @@ def _accept_cookie(request):
def install_filter(profile):
- """Install the cookie filter on the given profile.
-
- On Qt < 5.11, the filter isn't installed.
- """
- store = profile.cookieStore()
- try:
- store.setCookieFilter(_accept_cookie)
- except AttributeError:
- pass
+ """Install the cookie filter on the given profile."""
+ profile.cookieStore().setCookieFilter(_accept_cookie)
diff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py
new file mode 100644
index 000000000..d067edea3
--- /dev/null
+++ b/qutebrowser/browser/webengine/darkmode.py
@@ -0,0 +1,305 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2020 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 <http://www.gnu.org/licenses/>.
+
+"""Get darkmode arguments to pass to Qt.
+
+Overview of blink setting names based on the Qt version:
+
+Qt 5.10
+-------
+
+First implementation, called "high contrast mode".
+
+- highContrastMode (kOff/kSimpleInvertForTesting/kInvertBrightness/kInvertLightness)
+- highContrastGrayscale (bool)
+- highContrastContrast (float)
+- highContractImagePolicy (kFilterAll/kFilterNone)
+
+Qt 5.11, 5.12, 5.13
+-------------------
+
+New "smart" image policy.
+
+- Mode/Grayscale/Contrast as above
+- highContractImagePolicy (kFilterAll/kFilterNone/kFilterSmart [new!])
+
+Qt 5.14
+-------
+
+Renamed to "darkMode".
+
+- darkMode (kOff/kSimpleInvertForTesting/kInvertBrightness/kInvertLightness/
+ kInvertLightnessLAB [new!])
+- darkModeGrayscale (bool)
+- darkModeContrast (float)
+- darkModeImagePolicy (kFilterAll/kFilterNone/kFilterSmart)
+- darkModePagePolicy (kFilterAll/kFilterByBackground) [new!]
+- darkModeTextBrightnessThreshold (int) [new!]
+- darkModeBackgroundBrightnessThreshold (int) [new!]
+- darkModeImageGrayscale (float) [new!]
+
+Qt 5.15.0 and 5.15.1
+--------------------
+
+"darkMode" split into "darkModeEnabled" and "darkModeInversionAlgorithm".
+
+- darkModeEnabled (bool) [new!]
+- darkModeInversionAlgorithm (kSimpleInvertForTesting/kInvertBrightness/
+ kInvertLightness/kInvertLightnessLAB)
+- Rest (except darkMode) as above.
+- NOTE: smart image policy is broken with Qt 5.15.0!
+
+Qt 5.15.2
+---------
+
+Prefix changed to "forceDarkMode".
+
+- As with Qt 5.15.0 / .1, but with "forceDarkMode" as prefix.
+"""
+
+import os
+import enum
+from typing import Any, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union
+
+try:
+ from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION
+except ImportError: # pragma: no cover
+ # Added in PyQt 5.13
+ PYQT_WEBENGINE_VERSION = None # type: ignore[assignment]
+
+from qutebrowser.config import config
+from qutebrowser.utils import usertypes, qtutils, utils, log
+
+
+class Variant(enum.Enum):
+
+ """A dark mode variant."""
+
+ qt_511_to_513 = enum.auto()
+ qt_514 = enum.auto()
+ qt_515_0 = enum.auto()
+ qt_515_1 = enum.auto()
+ qt_515_2 = enum.auto()
+
+
+# Mapping from a colors.webpage.darkmode.algorithm setting value to
+# Chromium's DarkModeInversionAlgorithm enum values.
+_ALGORITHMS = {
+ # 0: kOff (not exposed)
+ # 1: kSimpleInvertForTesting (not exposed)
+ 'brightness-rgb': 2, # kInvertBrightness
+ 'lightness-hsl': 3, # kInvertLightness
+ 'lightness-cielab': 4, # kInvertLightnessLAB
+}
+# kInvertLightnessLAB is not available with Qt < 5.14
+_ALGORITHMS_BEFORE_QT_514 = _ALGORITHMS.copy()
+_ALGORITHMS_BEFORE_QT_514['lightness-cielab'] = _ALGORITHMS['lightness-hsl']
+
+# Mapping from a colors.webpage.darkmode.policy.images setting value to
+# Chromium's DarkModeImagePolicy enum values.
+_IMAGE_POLICIES = {
+ 'always': 0, # kFilterAll
+ 'never': 1, # kFilterNone
+ 'smart': 2, # kFilterSmart
+}
+
+# Mapping from a colors.webpage.darkmode.policy.page setting value to
+# Chromium's DarkModePagePolicy enum values.
+_PAGE_POLICIES = {
+ 'always': 0, # kFilterAll
+ 'smart': 1, # kFilterByBackground
+}
+
+_BOOLS = {
+ True: 'true',
+ False: 'false',
+}
+
+_DarkModeSettingsType = Iterable[
+ Tuple[
+ str, # qutebrowser option name
+ str, # darkmode setting name
+ # Mapping from the config value to a string (or something convertable
+ # to a string) which gets passed to Chromium.
+ Optional[Mapping[Any, Union[str, int]]],
+ ],
+]
+
+_DarkModeDefinitionType = Tuple[_DarkModeSettingsType, Set[str]]
+
+_QT_514_SETTINGS = [
+ ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'darkModeContrast', None),
+ ('grayscale.all', 'darkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'darkModeTextBrightnessThreshold', None),
+ ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None),
+ ('grayscale.images', 'darkModeImageGrayscale', None),
+]
+
+# Our defaults for policy.images are different from Chromium's, so we mark it as
+# mandatory setting - except on Qt 5.15.0 where we don't, so we don't get the
+# workaround warning below if the setting wasn't explicitly customized.
+
+_DARK_MODE_DEFINITIONS: Mapping[Variant, _DarkModeDefinitionType] = {
+ Variant.qt_515_2: ([
+ # 'darkMode' renamed to 'forceDarkMode'
+ ('enabled', 'forceDarkModeEnabled', _BOOLS),
+ ('algorithm', 'forceDarkModeInversionAlgorithm', _ALGORITHMS),
+
+ ('policy.images', 'forceDarkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'forceDarkModeContrast', None),
+ ('grayscale.all', 'forceDarkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'forceDarkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'forceDarkModeTextBrightnessThreshold', None),
+ (
+ 'threshold.background',
+ 'forceDarkModeBackgroundBrightnessThreshold',
+ None
+ ),
+ ('grayscale.images', 'forceDarkModeImageGrayscale', None),
+ ], {'enabled', 'policy.images'}),
+
+ Variant.qt_515_1: ([
+ # 'policy.images' mandatory again
+ ('enabled', 'darkModeEnabled', _BOOLS),
+ ('algorithm', 'darkModeInversionAlgorithm', _ALGORITHMS),
+
+ ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'darkModeContrast', None),
+ ('grayscale.all', 'darkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'darkModeTextBrightnessThreshold', None),
+ ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None),
+ ('grayscale.images', 'darkModeImageGrayscale', None),
+ ], {'enabled', 'policy.images'}),
+
+ Variant.qt_515_0: ([
+ # 'policy.images' not mandatory because it's broken
+ ('enabled', 'darkModeEnabled', _BOOLS),
+ ('algorithm', 'darkModeInversionAlgorithm', _ALGORITHMS),
+
+ ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'darkModeContrast', None),
+ ('grayscale.all', 'darkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'darkModeTextBrightnessThreshold', None),
+ ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None),
+ ('grayscale.images', 'darkModeImageGrayscale', None),
+ ], {'enabled'}),
+
+ Variant.qt_514: ([
+ ('algorithm', 'darkMode', _ALGORITHMS), # new: kInvertLightnessLAB
+
+ ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES),
+ ('contrast', 'darkModeContrast', None),
+ ('grayscale.all', 'darkModeGrayscale', _BOOLS),
+
+ ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES),
+ ('threshold.text', 'darkModeTextBrightnessThreshold', None),
+ ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None),
+ ('grayscale.images', 'darkModeImageGrayscale', None),
+ ], {'algorithm', 'policy.images'}),
+
+ Variant.qt_511_to_513: ([
+ ('algorithm', 'highContrastMode', _ALGORITHMS_BEFORE_QT_514),
+
+ ('policy.images', 'highContrastImagePolicy', _IMAGE_POLICIES), # new: smart
+ ('contrast', 'highContrastContrast', None),
+ ('grayscale.all', 'highContrastGrayscale', _BOOLS),
+ ], {'algorithm', 'policy.images'}),
+}
+
+
+def _variant() -> Variant:
+ """Get the dark mode variant based on the underlying Qt version."""
+ env_var = os.environ.get('QUTE_DARKMODE_VARIANT')
+ if env_var is not None:
+ try:
+ return Variant[env_var]
+ except KeyError:
+ log.init.warning(f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}")
+
+ if PYQT_WEBENGINE_VERSION is not None:
+ # Available with Qt >= 5.13
+ if PYQT_WEBENGINE_VERSION >= 0x050f02:
+ return Variant.qt_515_2
+ elif PYQT_WEBENGINE_VERSION == 0x050f01:
+ return Variant.qt_515_1
+ elif PYQT_WEBENGINE_VERSION == 0x050f00:
+ return Variant.qt_515_0
+ elif PYQT_WEBENGINE_VERSION >= 0x050e00:
+ return Variant.qt_514
+ elif PYQT_WEBENGINE_VERSION >= 0x050d00:
+ return Variant.qt_511_to_513
+ raise utils.Unreachable(hex(PYQT_WEBENGINE_VERSION))
+
+ # If we don't have PYQT_WEBENGINE_VERSION, we're on 5.12 (or older, but 5.12 is the
+ # oldest supported version).
+ assert not qtutils.version_check( # type: ignore[unreachable]
+ '5.13', compiled=False)
+
+ return Variant.qt_511_to_513
+
+
+def settings() -> Iterator[Tuple[str, str]]:
+ """Get necessary blink settings to configure dark mode for QtWebEngine."""
+ if (qtutils.version_check('5.15.2', compiled=False) and
+ config.val.colors.webpage.prefers_color_scheme_dark):
+ # With older Qt versions, this is passed in qtargs.py as --force-dark-mode
+ # instead.
+ #
+ # With Chromium 85 (> Qt 5.15.2), the enumeration has changed in Blink and this
+ # will need to be set to '0' instead:
+ # https://chromium-review.googlesource.com/c/chromium/src/+/2232922
+ yield "preferredColorScheme", "1"
+
+ if not config.val.colors.webpage.darkmode.enabled:
+ return
+
+ variant = _variant()
+ setting_defs, mandatory_settings = _DARK_MODE_DEFINITIONS[variant]
+
+ for setting, key, mapping in setting_defs:
+ # To avoid blowing up the commandline length, we only pass modified
+ # settings to Chromium, as our defaults line up with Chromium's.
+ # However, we always pass enabled/algorithm to make sure dark mode gets
+ # actually turned on.
+ value = config.instance.get(
+ 'colors.webpage.darkmode.' + setting,
+ fallback=setting in mandatory_settings)
+ if isinstance(value, usertypes.Unset):
+ continue
+
+ if (setting == 'policy.images' and value == 'smart' and
+ variant == Variant.qt_515_0):
+ # WORKAROUND for
+ # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211
+ log.init.warning("Ignoring colors.webpage.darkmode.policy.images = smart "
+ "because of Qt 5.15.0 bug")
+ continue
+
+ if mapping is not None:
+ value = mapping[value]
+
+ yield key, str(value)
diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py
index d4dcb522f..b27509552 100644
--- a/qutebrowser/browser/webengine/interceptor.py
+++ b/qutebrowser/browser/webengine/interceptor.py
@@ -25,7 +25,7 @@ from PyQt5.QtCore import QUrl, QByteArray
from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
QWebEngineUrlRequestInfo)
-from qutebrowser.config import websettings
+from qutebrowser.config import websettings, config
from qutebrowser.browser import shared
from qutebrowser.utils import utils, log, debug, qtutils
from qutebrowser.extensions import interceptors
@@ -39,9 +39,9 @@ class WebEngineRequest(interceptors.Request):
_WHITELISTED_REQUEST_METHODS = {QByteArray(b'GET'), QByteArray(b'HEAD')}
- _webengine_info = attr.ib(default=None) # type: QWebEngineUrlRequestInfo
+ _webengine_info: QWebEngineUrlRequestInfo = attr.ib(default=None)
#: If this request has been redirected already
- _redirected = attr.ib(init=False, default=False) # type: bool
+ _redirected: bool = attr.ib(init=False, default=False)
def redirect(self, url: QUrl) -> None:
if self._redirected:
@@ -129,7 +129,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
# Qt >= 5.13, GUI thread
profile.setUrlRequestInterceptor(self)
except AttributeError:
- # Qt <= 5.12, IO thread
+ # Qt 5.12, IO thread
profile.setRequestInterceptor(self)
# Gets called in the IO thread -> showing crash window will fail
@@ -204,5 +204,11 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
for header, value in shared.custom_headers(url=url):
info.setHttpHeader(header, value)
+ # Note this is ignored before Qt 5.12.4 and 5.13.1 due to
+ # https://bugreports.qt.io/browse/QTBUG-60203 - there, we set the
+ # commandline-flag in qtargs.py instead.
+ if config.cache['content.headers.referer'] == 'never':
+ info.setHttpHeader(b'Referer', b'')
+
user_agent = websettings.user_agent(url)
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))
diff --git a/qutebrowser/browser/webengine/spell.py b/qutebrowser/browser/webengine/spell.py
index 76f1c86a6..417eae9aa 100644
--- a/qutebrowser/browser/webengine/spell.py
+++ b/qutebrowser/browser/webengine/spell.py
@@ -24,23 +24,12 @@ import glob
import os
import os.path
import re
-import shutil
-from PyQt5.QtCore import QLibraryInfo
-from qutebrowser.utils import log, message, standarddir, qtutils
+from qutebrowser.utils import log, message, standarddir
_DICT_VERSION_RE = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
-def can_use_data_path():
- """Whether the current Qt version can use a customized path.
-
- Qt >= 5.10 understands QTWEBENGINE_DICTIONARIES_PATH which means we don't
- need to put them to a fixed root-only folder.
- """
- return qtutils.version_check('5.10', compiled=False)
-
-
def version(filename):
"""Extract the version number from the dictionary file name."""
match = _DICT_VERSION_RE.fullmatch(filename)
@@ -51,13 +40,9 @@ def version(filename):
return tuple(int(n) for n in match.group('version').split('-'))
-def dictionary_dir(old=False):
+def dictionary_dir():
"""Return the path (str) to the QtWebEngine's dictionaries directory."""
- if can_use_data_path() and not old:
- datapath = standarddir.data()
- else:
- datapath = QLibraryInfo.location(QLibraryInfo.DataPath)
- return os.path.join(datapath, 'qtwebengine_dictionaries')
+ return os.path.join(standarddir.data(), 'qtwebengine_dictionaries')
def local_files(code):
@@ -91,13 +76,6 @@ def local_filename(code):
def init():
- """Initialize the dictionary path if supported."""
- if can_use_data_path():
- new_dir = dictionary_dir()
- old_dir = dictionary_dir(old=True)
- os.environ['QTWEBENGINE_DICTIONARIES_PATH'] = new_dir
- try:
- if os.path.exists(old_dir) and not os.path.exists(new_dir):
- shutil.copytree(old_dir, new_dir)
- except OSError:
- log.misc.exception("Failed to copy old dictionaries")
+ """Initialize the dictionary path."""
+ dict_dir = dictionary_dir()
+ os.environ['QTWEBENGINE_DICTIONARIES_PATH'] = dict_dir
diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py
index 16c7f020a..ef592c98c 100644
--- a/qutebrowser/browser/webengine/webenginedownloads.py
+++ b/qutebrowser/browser/webengine/webenginedownloads.py
@@ -21,14 +21,13 @@
import re
import os.path
-import urllib
import functools
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QObject
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
from qutebrowser.browser import downloads, pdfjs
-from qutebrowser.utils import debug, usertypes, message, log, qtutils, objreg
+from qutebrowser.utils import debug, usertypes, message, log, objreg
class DownloadItem(downloads.AbstractDownloadItem):
@@ -84,12 +83,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.stats.finish()
elif state == QWebEngineDownloadItem.DownloadInterrupted:
self.successful = False
- # https://bugreports.qt.io/browse/QTBUG-56839
- try:
- reason = self._qt_item.interruptReasonString()
- except AttributeError:
- # Qt < 5.9
- reason = "Download failed"
+ reason = self._qt_item.interruptReasonString()
self._die(reason)
else:
raise ValueError("_on_state_changed was called with unknown state "
@@ -102,22 +96,21 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._qt_item.cancel()
def _do_cancel(self):
+ state = self._qt_item.state()
+ state_name = debug.qenum_key(QWebEngineDownloadItem, state)
+ assert state not in [QWebEngineDownloadItem.DownloadCompleted,
+ QWebEngineDownloadItem.DownloadCancelled], state_name
self._qt_item.cancel()
def retry(self):
state = self._qt_item.state()
if state != QWebEngineDownloadItem.DownloadInterrupted:
log.downloads.warning(
- "Trying to retry download in state {}".format(
+ "Refusing to retry download in state {}".format(
debug.qenum_key(QWebEngineDownloadItem, state)))
return
- try:
- self._qt_item.resume()
- except AttributeError:
- raise downloads.UnsupportedOperationError(
- "Retrying downloads is unsupported with QtWebEngine on "
- "Qt/PyQt < 5.10")
+ self._qt_item.resume()
def _get_open_filename(self):
return self._filename
@@ -233,14 +226,7 @@ def _get_suggested_filename(path):
(?=\.|$) # Begin of extension, or filename without extension
""", re.VERBOSE)
- filename = suffix_re.sub('', filename)
- if not qtutils.version_check('5.9', compiled=False):
- # https://bugreports.qt.io/browse/QTBUG-58155
- filename = urllib.parse.unquote(filename)
- # Doing basename a *second* time because there could be a %2F in
- # there...
- filename = os.path.basename(filename)
- return filename
+ return suffix_re.sub('', filename)
class DownloadManager(downloads.AbstractDownloadManager):
diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py
index db5335a3c..37b9de364 100644
--- a/qutebrowser/browser/webengine/webengineelem.py
+++ b/qutebrowser/browser/webengine/webengineelem.py
@@ -19,17 +19,17 @@
"""QtWebEngine specific part of the web element API."""
-import typing
+from typing import (
+ TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Set, Tuple, Union)
-from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop
-from PyQt5.QtGui import QMouseEvent
+from PyQt5.QtCore import QRect, QEventLoop
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
from qutebrowser.utils import log, javascript, urlutils, usertypes, utils
from qutebrowser.browser import webelem
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser.webengine import webenginetab
@@ -37,11 +37,11 @@ class WebEngineElement(webelem.AbstractWebElement):
"""A web element for QtWebEngine, using JS under the hood."""
- def __init__(self, js_dict: typing.Dict[str, typing.Any],
+ def __init__(self, js_dict: Dict[str, Any],
tab: 'webenginetab.WebEngineTab') -> None:
super().__init__(tab)
# Do some sanity checks on the data we get from JS
- js_dict_types = {
+ js_dict_types: Dict[str, Union[type, Tuple[type, ...]]] = {
'id': int,
'text': str,
'value': (str, int, float),
@@ -52,7 +52,7 @@ class WebEngineElement(webelem.AbstractWebElement):
'attributes': dict,
'is_content_editable': bool,
'caret_position': (int, type(None)),
- } # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]]
+ }
assert set(js_dict.keys()).issubset(js_dict_types.keys())
for name, typ in js_dict_types.items():
if name in js_dict and not isinstance(js_dict[name], typ):
@@ -97,14 +97,14 @@ class WebEngineElement(webelem.AbstractWebElement):
utils.unused(key)
log.stub()
- def __iter__(self) -> typing.Iterator[str]:
+ def __iter__(self) -> Iterator[str]:
return iter(self._js_dict['attributes'])
def __len__(self) -> int:
return len(self._js_dict['attributes'])
def _js_call(self, name: str, *args: webelem.JsValueType,
- callback: typing.Callable[[typing.Any], None] = None) -> None:
+ callback: Callable[[Any], None] = None) -> None:
"""Wrapper to run stuff from webelem.js."""
if self._tab.is_deleted():
raise webelem.OrphanedError("Tab containing element vanished")
@@ -118,9 +118,9 @@ class WebEngineElement(webelem.AbstractWebElement):
log.stub()
return QRect()
- def classes(self) -> typing.List[str]:
+ def classes(self) -> Set[str]:
"""Get a list of classes assigned to this element."""
- return self._js_dict['class_name'].split()
+ return set(self._js_dict['class_name'].split())
def tag_name(self) -> str:
"""Get the tag name of this element.
@@ -150,7 +150,7 @@ class WebEngineElement(webelem.AbstractWebElement):
composed: bool = False) -> None:
self._js_call('dispatch_event', event, bubbles, cancelable, composed)
- def caret_position(self) -> typing.Optional[int]:
+ def caret_position(self) -> Optional[int]:
"""Get the text caret position for the current element.
If the element is not a text element, None is returned.
@@ -229,11 +229,7 @@ class WebEngineElement(webelem.AbstractWebElement):
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
- ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
- QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton,
- Qt.NoModifier, Qt.MouseEventSynthesizedBySystem)
- self._tab.send_event(ev)
+ self._tab.setFocus() # Needed as WORKAROUND on Qt 5.12
# This actually "clicks" the element by calling focus() on it in JS.
self._js_call('focus')
self._move_text_cursor()
@@ -256,7 +252,7 @@ class WebEngineElement(webelem.AbstractWebElement):
QEventLoop.ExcludeSocketNotifiers |
QEventLoop.ExcludeUserInputEvents)
- def reset_setting(_arg: typing.Any) -> None:
+ def reset_setting(_arg: Any) -> None:
"""Set the JavascriptCanOpenWindows setting to its old value."""
assert view is not None
try:
diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py
index f84415c65..5685a9538 100644
--- a/qutebrowser/browser/webengine/webengineinspector.py
+++ b/qutebrowser/browser/webengine/webengineinspector.py
@@ -19,18 +19,16 @@
"""Customized QWebInspector for QtWebEngine."""
-import os
-import typing
import pathlib
-from PyQt5.QtCore import QUrl, QLibraryInfo
+from PyQt5.QtCore import QLibraryInfo
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtWidgets import QWidget
from qutebrowser.browser import inspector
from qutebrowser.browser.webengine import webenginesettings
from qutebrowser.misc import miscwidgets
-from qutebrowser.utils import version, qtutils
+from qutebrowser.utils import version
class WebEngineInspectorView(QWebEngineView):
@@ -53,47 +51,9 @@ class WebEngineInspectorView(QWebEngineView):
return self.page().inspectedPage().view().createWindow(wintype)
-def supports_new() -> bool:
- """Check whether a new-style inspector is supported."""
- return hasattr(QWebEnginePage, 'setInspectedPage')
-
-
-class LegacyWebEngineInspector(inspector.AbstractWebInspector):
-
- """A web inspector for QtWebEngine without Qt API support.
-
- Only needed with Qt <= 5.10.
- """
-
- def __init__(self, splitter: miscwidgets.InspectorSplitter,
- win_id: int,
- parent: QWidget = None) -> None:
- super().__init__(splitter, win_id, parent)
- self._ensure_enabled()
- view = WebEngineInspectorView()
- self._settings = webenginesettings.WebEngineSettings(view.settings())
- self._set_widget(view)
-
- def _ensure_enabled(self) -> None:
- if 'QTWEBENGINE_REMOTE_DEBUGGING' not in os.environ:
- raise inspector.Error(
- "QtWebEngine inspector is not enabled. See "
- "'qutebrowser --help' for details.")
-
- def inspect(self, page: QWebEnginePage) -> None: # type: ignore[override]
- # We're lying about the URL here a bit, but this way, URL patterns for
- # Qt 5.11/5.12/5.13 also work in this case.
- self._settings.update_for_url(QUrl('chrome-devtools://devtools'))
- port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING'])
- self._widget.load(QUrl('http://localhost:{}/'.format(port)))
-
-
class WebEngineInspector(inspector.AbstractWebInspector):
- """A web inspector for QtWebEngine with Qt API support.
-
- Available since Qt 5.11.
- """
+ """A web inspector for QtWebEngine with Qt API support."""
def __init__(self, splitter: miscwidgets.InspectorSplitter,
win_id: int,
@@ -118,7 +78,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
pak = data_path / 'resources' / 'qtwebengine_devtools_resources.pak'
if not pak.exists():
raise inspector.Error("QtWebEngine devtools resources not found, "
- "please install the qt5-webengine-devtools "
+ "please install the qt5-qtwebengine-devtools "
"Fedora package.")
def inspect(self, page: QWebEnginePage) -> None: # type: ignore[override]
@@ -131,4 +91,4 @@ class WebEngineInspector(inspector.AbstractWebInspector):
WORKAROUND for what's likely an unknown Qt bug.
"""
- return qtutils.version_check('5.12')
+ return True
diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py
index 879f8aeca..f1eebd75d 100644
--- a/qutebrowser/browser/webengine/webenginequtescheme.py
+++ b/qutebrowser/browser/webengine/webenginequtescheme.py
@@ -21,12 +21,8 @@
from PyQt5.QtCore import QBuffer, QIODevice, QUrl
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
- QWebEngineUrlRequestJob)
-try:
- from PyQt5.QtWebEngineCore import QWebEngineUrlScheme
-except ImportError:
- # Added in Qt 5.12
- QWebEngineUrlScheme = None # type: ignore[misc, assignment]
+ QWebEngineUrlRequestJob,
+ QWebEngineUrlScheme)
from qutebrowser.browser import qutescheme
from qutebrowser.utils import log, qtutils
@@ -42,11 +38,6 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
assert QWebEngineUrlScheme.schemeByName(b'qute') is not None
profile.installUrlSchemeHandler(b'qute', self)
- if (qtutils.version_check('5.11', compiled=False) and
- not qtutils.version_check('5.12', compiled=False)):
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
- profile.installUrlSchemeHandler(b'chrome-error', self)
- profile.installUrlSchemeHandler(b'chrome-extension', self)
def _check_initiator(self, job):
"""Check whether the initiator of the job should be allowed.
@@ -60,29 +51,16 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
Return:
True if the initiator is allowed, False if it was blocked.
"""
- try:
- initiator = job.initiator()
- request_url = job.requestUrl()
- except AttributeError:
- # Added in Qt 5.11
- return True
+ initiator = job.initiator()
+ request_url = job.requestUrl()
# https://codereview.qt-project.org/#/c/234849/
is_opaque = initiator == QUrl('null')
target = request_url.scheme(), request_url.host()
- if is_opaque and not qtutils.version_check('5.12'):
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70421
- # When we don't register the qute:// scheme, all requests are
- # flagged as opaque.
- return True
-
- if (target == ('qute', 'testdata') and
- is_opaque and
- qtutils.version_check('5.12')):
- # Allow requests to qute://testdata, as this is needed in Qt 5.12
- # for all tests to work properly. No qute://testdata handler is
- # installed outside of tests.
+ if target == ('qute', 'testdata') and is_opaque:
+ # Allow requests to qute://testdata, as this is needed for all tests to work
+ # properly. No qute://testdata handler is installed outside of tests.
return True
if initiator.isValid() and initiator.scheme() != 'qute':
@@ -105,11 +83,6 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
"""
url = job.requestUrl()
- if url.scheme() in ['chrome-error', 'chrome-extension']:
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
- job.fail(QWebEngineUrlRequestJob.UrlInvalid)
- return
-
if not self._check_initiator(job):
return
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index f5f4e9c31..1526574a7 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -26,24 +26,22 @@ Module attributes:
import os
import operator
-import typing
+from typing import cast, Any, List, Optional, Tuple, Union
from PyQt5.QtGui import QFont
-from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
- QWebEnginePage)
+from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineProfile
from qutebrowser.browser.webengine import spell, webenginequtescheme
from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
-from qutebrowser.utils import (utils, standarddir, qtutils, message, log,
- urlmatch, usertypes)
+from qutebrowser.utils import standarddir, qtutils, message, log, urlmatch, usertypes
# The default QWebEngineProfile
-default_profile = typing.cast(QWebEngineProfile, None)
+default_profile = cast(QWebEngineProfile, None)
# The QWebEngineProfile used for private (off-the-record) windows
-private_profile = None # type: typing.Optional[QWebEngineProfile]
+private_profile: Optional[QWebEngineProfile] = None
# The global WebEngineSettings object
-global_settings = typing.cast('WebEngineSettings', None)
+global_settings = cast('WebEngineSettings', None)
parsed_user_agent = None
@@ -126,8 +124,7 @@ class WebEngineSettings(websettings.AbstractSettings):
'content.desktop_capture':
Attr(QWebEngineSettings.ScreenCaptureEnabled,
converter=lambda val: True if val == 'ask' else val),
- # 'ask' is handled via the permission system,
- # or a hardcoded dialog on Qt < 5.10
+ # 'ask' is handled via the permission system
'input.spatial_navigation':
Attr(QWebEngineSettings.SpatialNavigationEnabled),
@@ -136,6 +133,16 @@ class WebEngineSettings(websettings.AbstractSettings):
'scrolling.smooth':
Attr(QWebEngineSettings.ScrollAnimatorEnabled),
+
+ 'content.print_element_backgrounds':
+ Attr(QWebEngineSettings.PrintElementBackgrounds),
+
+ 'content.autoplay':
+ Attr(QWebEngineSettings.PlaybackRequiresUserGesture,
+ converter=operator.not_),
+
+ 'content.dns_prefetch':
+ Attr(QWebEngineSettings.DnsPrefetchEnabled),
}
_FONT_SIZES = {
@@ -158,18 +165,14 @@ class WebEngineSettings(websettings.AbstractSettings):
'fonts.web.family.fantasy': QWebEngineSettings.FantasyFont,
}
- # Only Qt >= 5.11 support UnknownUrlSchemePolicy
- try:
- _UNKNOWN_URL_SCHEME_POLICY = {
- 'disallow':
- QWebEngineSettings.DisallowUnknownUrlSchemes,
- 'allow-from-user-interaction':
- QWebEngineSettings.AllowUnknownUrlSchemesFromUserInteraction,
- 'allow-all':
- QWebEngineSettings.AllowAllUnknownUrlSchemes,
- }
- except AttributeError:
- _UNKNOWN_URL_SCHEME_POLICY = None
+ _UNKNOWN_URL_SCHEME_POLICY = {
+ 'disallow':
+ QWebEngineSettings.DisallowUnknownUrlSchemes,
+ 'allow-from-user-interaction':
+ QWebEngineSettings.AllowUnknownUrlSchemesFromUserInteraction,
+ 'allow-all':
+ QWebEngineSettings.AllowAllUnknownUrlSchemes,
+ }
# Mapping from WebEngineSettings::initDefaults in
# qtwebengine/src/core/web_engine_settings.cpp
@@ -183,7 +186,7 @@ class WebEngineSettings(websettings.AbstractSettings):
}
def set_unknown_url_scheme_policy(
- self, policy: typing.Union[str, usertypes.Unset]) -> bool:
+ self, policy: Union[str, usertypes.Unset]) -> bool:
"""Set the UnknownUrlSchemePolicy to use.
Return:
@@ -200,39 +203,13 @@ class WebEngineSettings(websettings.AbstractSettings):
def _update_setting(self, setting, value):
if setting == 'content.unknown_url_scheme_policy':
- if self._UNKNOWN_URL_SCHEME_POLICY:
- return self.set_unknown_url_scheme_policy(value)
- return False
+ return self.set_unknown_url_scheme_policy(value)
return super()._update_setting(setting, value)
def init_settings(self):
super().init_settings()
self.update_setting('content.unknown_url_scheme_policy')
- def __init__(self, settings):
- super().__init__(settings)
- # Attributes which don't exist in all Qt versions.
- new_attributes = {
- # Qt 5.8
- 'content.print_element_backgrounds':
- ('PrintElementBackgrounds', None),
-
- # Qt 5.11
- 'content.autoplay':
- ('PlaybackRequiresUserGesture', operator.not_),
-
- # Qt 5.12
- 'content.dns_prefetch':
- ('DnsPrefetchEnabled', None),
- }
- for name, (attribute, converter) in new_attributes.items():
- try:
- value = getattr(QWebEngineSettings, attribute)
- except AttributeError:
- continue
-
- self._ATTRIBUTES[name] = Attr(value, converter=converter)
-
class ProfileSetter:
@@ -246,8 +223,7 @@ class ProfileSetter:
self.set_http_headers()
self.set_http_cache_size()
self._set_hardcoded_settings()
- if qtutils.version_check('5.8'):
- self.set_dictionary_language()
+ self.set_dictionary_language()
def _set_hardcoded_settings(self):
"""Set up settings with a fixed value."""
@@ -255,13 +231,8 @@ class ProfileSetter:
settings.setAttribute(
QWebEngineSettings.FullScreenSupportEnabled, True)
-
- try:
- settings.setAttribute(
- QWebEngineSettings.FocusOnNavigationEnabled, False)
- except AttributeError:
- # Added in Qt 5.8
- pass
+ settings.setAttribute(
+ QWebEngineSettings.FocusOnNavigationEnabled, False)
try:
settings.setAttribute(QWebEngineSettings.PdfViewerEnabled, False)
@@ -328,8 +299,7 @@ def _update_settings(option):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884
# (note this isn't actually fixed properly before Qt 5.15)
- header_bug_fixed = (not qtutils.version_check('5.12', compiled=False) or
- qtutils.version_check('5.15', compiled=False))
+ header_bug_fixed = qtutils.version_check('5.15', compiled=False)
if option in ['content.headers.user_agent',
'content.headers.accept_language'] and header_bug_fixed:
@@ -340,9 +310,7 @@ def _update_settings(option):
default_profile.setter.set_http_cache_size()
if private_profile:
private_profile.setter.set_http_cache_size()
- elif (option == 'content.cookies.store' and
- # https://bugreports.qt.io/browse/QTBUG-58650
- qtutils.version_check('5.9', compiled=False)):
+ elif option == 'content.cookies.store':
default_profile.setter.set_persistent_cookie_policy()
# We're not touching the private profile's cookie policy.
elif option == 'spellcheck.languages':
@@ -390,9 +358,14 @@ def init_private_profile():
def _init_site_specific_quirks():
+ """Add custom user-agent settings for problematic sites.
+
+ See https://github.com/qutebrowser/qutebrowser/issues/4810
+ """
if not config.val.content.site_specific_quirks:
return
+ # Please leave this here as a template for new UAs.
# default_ua = ("Mozilla/5.0 ({os_info}) "
# "AppleWebKit/{webkit_version} (KHTML, like Gecko) "
# "{qt_key}/{qt_version} "
@@ -402,7 +375,6 @@ def _init_site_specific_quirks():
"AppleWebKit/{webkit_version} (KHTML, like Gecko) "
"{upstream_browser_key}/{upstream_browser_version} "
"Safari/{webkit_version}")
- firefox_ua = "Mozilla/5.0 ({os_info}; rv:71.0) Gecko/20100101 Firefox/71.0"
new_chrome_ua = ("Mozilla/5.0 ({os_info}) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/99 "
@@ -414,16 +386,24 @@ def _init_site_specific_quirks():
"Edg/{upstream_browser_version}")
user_agents = {
+ # Needed to avoid a ""WhatsApp works with Google Chrome 36+" error
+ # page which doesn't allow to use WhatsApp Web at all. Also see the
+ # additional JS quirk: qutebrowser/javascript/whatsapp_web_quirk.user.js
+ # https://github.com/qutebrowser/qutebrowser/issues/4445
'https://web.whatsapp.com/': no_qtwe_ua,
+
+ # 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
'https://accounts.google.com/*': edge_ua,
+
+ # Needed because Slack adds an error which prevents using it relatively
+ # aggressively, despite things actually working fine.
+ # September 2020: Qt 5.12 works, but Qt <= 5.11 shows the error.
+ # https://github.com/qutebrowser/qutebrowser/issues/4669
'https://*.slack.com/*': new_chrome_ua,
- 'https://docs.google.com/*': firefox_ua,
- 'https://drive.google.com/*': firefox_ua,
}
- if not qtutils.version_check('5.9'):
- user_agents['https://www.dell.com/support/*'] = new_chrome_ua
-
for pattern, ua in user_agents.items():
config.instance.set_obj('content.headers.user_agent', ua,
pattern=urlmatch.UrlPattern(pattern),
@@ -432,12 +412,11 @@ def _init_site_specific_quirks():
def _init_devtools_settings():
"""Make sure the devtools always get images/JS permissions."""
- settings = [
+ settings: List[Tuple[str, Any]] = [
('content.javascript.enabled', True),
- ('content.images', True)
- ] # type: typing.List[typing.Tuple[str, typing.Any]]
- if qtutils.version_check('5.11'):
- settings.append(('content.cookies.accept', 'all'))
+ ('content.images', True),
+ ('content.cookies.accept', 'all'),
+ ]
for setting, value in settings:
for pattern in ['chrome-devtools://*', 'devtools://*']:
@@ -446,12 +425,8 @@ def _init_devtools_settings():
hide_userconfig=True)
-def init(args):
+def init():
"""Initialize the global QWebSettings."""
- if (args.enable_webengine_inspector and
- not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11
- os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
-
webenginequtescheme.init()
spell.init()
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index a139f3d2f..f026b7c23 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -23,15 +23,14 @@ import math
import functools
import re
import html as html_utils
-import typing
+from typing import cast, Union
-from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
- QTimer, QObject)
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl, QObject
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication, QWidget
-from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
+from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript, QWebEngineHistory
-from qutebrowser.config import configdata, config
+from qutebrowser.config import config
from qutebrowser.browser import (browsertab, eventfilter, shared, webelem,
history, greasemonkey)
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
@@ -41,7 +40,6 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
from qutebrowser.misc import miscwidgets, objects, quitter
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, objreg, jinja, debug)
-from qutebrowser.keyinput import modeman
from qutebrowser.qt import sip
@@ -119,18 +117,7 @@ class WebEngineAction(browsertab.AbstractAction):
self._show_source_pygments()
return
- try:
- self._widget.triggerPageAction(QWebEnginePage.ViewSource)
- except AttributeError:
- # Qt < 5.8
- tb = objreg.get('tabbed-browser', scope='window',
- window=self._tab.win_id)
- urlstr = self._tab.url().toString(
- QUrl.RemoveUserInfo) # type: ignore[arg-type]
- # The original URL becomes the path of a view-source: URL
- # (without a host), but query/fragment should stay.
- url = QUrl('view-source:' + urlstr)
- tb.tabopen(url, background=False, related=True)
+ self._widget.triggerPageAction(QWebEnginePage.ViewSource)
class WebEnginePrinting(browsertab.AbstractPrinting):
@@ -140,11 +127,6 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
def check_pdf_support(self):
pass
- def check_printer_support(self):
- if not hasattr(self._widget.page(), 'print'):
- raise browsertab.WebTabError(
- "Printing is unsupported with QtWebEngine on Qt < 5.8")
-
def check_preview_support(self):
raise browsertab.WebTabError(
"Print previews are unsupported with QtWebEngine")
@@ -356,18 +338,11 @@ class WebEngineCaret(browsertab.AbstractCaret):
"""QtWebEngine implementations related to moving the cursor/selection."""
- def __init__(self,
- tab: 'WebEngineTab',
- mode_manager: modeman.ModeManager,
- parent: QWidget = None) -> None:
- super().__init__(mode_manager, parent)
- self._tab = tab
+ _tab: 'WebEngineTab'
def _flags(self):
"""Get flags to pass to JS."""
flags = set()
- if qtutils.version_check('5.7.1', compiled=False):
- flags.add('filter-prefix')
if utils.is_windows:
flags.add('windows')
return list(flags)
@@ -380,7 +355,6 @@ class WebEngineCaret(browsertab.AbstractCaret):
if self._tab.search.search_displayed:
# We are currently in search mode.
# convert the search to a blue selection so we can operate on it
- # https://bugreports.qt.io/browse/QTBUG-60673
self._tab.search.clear()
self._tab.run_js_async(
@@ -513,7 +487,6 @@ class WebEngineCaret(browsertab.AbstractCaret):
if self._tab.search.search_displayed:
# We are currently in search mode.
# let's click the link via a fake-click
- # https://bugreports.qt.io/browse/QTBUG-60673
self._tab.search.clear()
log.webview.debug("Clicking a searched link via fake key press.")
@@ -674,16 +647,11 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""History-related methods which are not part of the extension API."""
+ def __init__(self, tab: 'WebEngineTab') -> None:
+ self._tab = tab
+ self._history = cast(QWebEngineHistory, None)
+
def serialize(self):
- if not qtutils.version_check('5.9', compiled=False):
- # WORKAROUND for
- # https://github.com/qutebrowser/qutebrowser/issues/2289
- # Don't use the history's currentItem here, because of
- # https://bugreports.qt.io/browse/QTBUG-59599 and because it doesn't
- # contain view-source.
- scheme = self._tab.url().scheme()
- if scheme in ['view-source', 'chrome']:
- raise browsertab.WebTabError("Can't serialize special URL!")
return qtutils.serialize(self._history)
def deserialize(self, data):
@@ -782,9 +750,7 @@ class WebEngineElements(browsertab.AbstractElements):
"""QtWebEngine implemementations related to elements on the page."""
- def __init__(self, tab: 'WebEngineTab') -> None:
- super().__init__()
- self._tab = tab
+ _tab: 'WebEngineTab'
def _js_cb_multiple(self, callback, error_cb, js_elems):
"""Handle found elements coming from JS and call the real callback.
@@ -916,8 +882,10 @@ class _WebEnginePermissions(QObject):
QWebEnginePage.Geolocation: 'content.geolocation',
QWebEnginePage.MediaAudioCapture: 'content.media.audio_capture',
QWebEnginePage.MediaVideoCapture: 'content.media.video_capture',
- QWebEnginePage.MediaAudioVideoCapture:
- 'content.media.audio_video_capture',
+ QWebEnginePage.MediaAudioVideoCapture: 'content.media.audio_video_capture',
+ QWebEnginePage.MouseLock: 'content.mouse_lock',
+ QWebEnginePage.DesktopVideoCapture: 'content.desktop_capture',
+ QWebEnginePage.DesktopAudioVideoCapture: 'content.desktop_capture',
}
_messages = {
@@ -926,42 +894,15 @@ class _WebEnginePermissions(QObject):
QWebEnginePage.MediaAudioCapture: 'record audio',
QWebEnginePage.MediaVideoCapture: 'record video',
QWebEnginePage.MediaAudioVideoCapture: 'record audio/video',
+ QWebEnginePage.MouseLock: 'hide your mouse pointer',
+ QWebEnginePage.DesktopVideoCapture: 'capture your desktop',
+ QWebEnginePage.DesktopAudioVideoCapture: 'capture your desktop and audio',
}
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
-
- try:
- self._options.update({
- QWebEnginePage.MouseLock:
- 'content.mouse_lock',
- })
- self._messages.update({
- QWebEnginePage.MouseLock:
- 'hide your mouse pointer',
- })
- except AttributeError:
- # Added in Qt 5.8
- pass
- try:
- self._options.update({
- QWebEnginePage.DesktopVideoCapture:
- 'content.desktop_capture',
- QWebEnginePage.DesktopAudioVideoCapture:
- 'content.desktop_capture',
- })
- self._messages.update({
- QWebEnginePage.DesktopVideoCapture:
- 'capture your desktop',
- QWebEnginePage.DesktopAudioVideoCapture:
- 'capture your desktop and audio',
- })
- except AttributeError:
- # Added in Qt 5.10
- pass
-
+ self._widget = cast(QWidget, None)
assert self._options.keys() == self._messages.keys()
def connect_signals(self):
@@ -972,11 +913,9 @@ class _WebEnginePermissions(QObject):
page.featurePermissionRequested.connect(
self._on_feature_permission_requested)
- if qtutils.version_check('5.11'):
- page.quotaRequested.connect(
- self._on_quota_requested)
- page.registerProtocolHandlerRequested.connect(
- self._on_register_protocol_handler_requested)
+ page.quotaRequested.connect(self._on_quota_requested)
+ page.registerProtocolHandlerRequested.connect(
+ self._on_register_protocol_handler_requested)
@pyqtSlot('QWebEngineFullScreenRequest')
def _on_fullscreen_requested(self, request):
@@ -1025,7 +964,6 @@ class _WebEnginePermissions(QObject):
return
if (
- hasattr(QWebEnginePage, 'DesktopVideoCapture') and
feature in [QWebEnginePage.DesktopVideoCapture,
QWebEnginePage.DesktopAudioVideoCapture] and
qtutils.version_check('5.13', compiled=False) and
@@ -1087,7 +1025,7 @@ class _WebEngineScripts(QObject):
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
- self._widget = typing.cast(QWidget, None)
+ self._widget = cast(QWidget, None)
self._greasemonkey = greasemonkey.gm_manager
def connect_signals(self):
@@ -1114,35 +1052,21 @@ class _WebEngineScripts(QObject):
def _inject_early_js(self, name, js_code, *,
world=QWebEngineScript.ApplicationWorld,
subframes=False):
- """Inject the given script to run early on a page load.
-
- This runs the script both on DocumentCreation and DocumentReady as on
- some internal pages, DocumentCreation will not work.
-
- That is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66011
- """
- scripts = self._widget.page().scripts()
- for injection in ['creation', 'ready']:
- injection_points = {
- 'creation': QWebEngineScript.DocumentCreation,
- 'ready': QWebEngineScript.DocumentReady,
- }
- script = QWebEngineScript()
- script.setInjectionPoint(injection_points[injection])
- script.setSourceCode(js_code)
- script.setWorldId(world)
- script.setRunsOnSubFrames(subframes)
- script.setName('_qute_{}_{}'.format(name, injection))
- scripts.insert(script)
+ """Inject the given script to run early on a page load."""
+ script = QWebEngineScript()
+ script.setInjectionPoint(QWebEngineScript.DocumentCreation)
+ script.setSourceCode(js_code)
+ script.setWorldId(world)
+ script.setRunsOnSubFrames(subframes)
+ script.setName(f'_qute_{name}')
+ self._widget.page().scripts().insert(script)
def _remove_early_js(self, name):
"""Remove an early QWebEngineScript."""
scripts = self._widget.page().scripts()
- for injection in ['creation', 'ready']:
- full_name = '_qute_{}_{}'.format(name, injection)
- script = scripts.findScript(full_name)
- if not script.isNull():
- scripts.remove(script)
+ script = scripts.findScript(f'_qute_{name}')
+ if not script.isNull():
+ scripts.remove(script)
def init(self):
"""Initialize global qutebrowser JavaScript."""
@@ -1152,29 +1076,14 @@ class _WebEngineScripts(QObject):
utils.read_file('javascript/webelem.js'),
utils.read_file('javascript/caret.js'),
)
- if not qtutils.version_check('5.12'):
- # WORKAROUND for Qt versions < 5.12 not exposing window.print().
- # Qt 5.12 has a printRequested() signal so we don't need this hack
- # anymore.
- self._inject_early_js('js',
- utils.read_file('javascript/print.js'),
- subframes=True,
- world=QWebEngineScript.MainWorld)
# FIXME:qtwebengine what about subframes=True?
self._inject_early_js('js', js_code, subframes=True)
self._init_stylesheet()
- # The Greasemonkey metadata block support in QtWebEngine only starts at
- # Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
- # response to urlChanged.
- if not qtutils.version_check('5.8'):
- self._tab.url_changed.connect(
- self._inject_greasemonkey_scripts_for_url)
- else:
- self._greasemonkey.scripts_reloaded.connect(
- self._inject_all_greasemonkey_scripts)
- self._inject_all_greasemonkey_scripts()
- self._inject_site_specific_quirks()
+ self._greasemonkey.scripts_reloaded.connect(
+ self._inject_all_greasemonkey_scripts)
+ self._inject_all_greasemonkey_scripts()
+ self._inject_site_specific_quirks()
def _init_stylesheet(self):
"""Initialize custom stylesheets.
@@ -1191,16 +1100,6 @@ class _WebEngineScripts(QObject):
)
self._inject_early_js('stylesheet', js_code, subframes=True)
- @pyqtSlot(QUrl)
- def _inject_greasemonkey_scripts_for_url(self, url):
- matching_scripts = self._greasemonkey.scripts_for(url)
- self._inject_greasemonkey_scripts(
- matching_scripts.start, QWebEngineScript.DocumentCreation, True)
- self._inject_greasemonkey_scripts(
- matching_scripts.end, QWebEngineScript.DocumentReady, False)
- self._inject_greasemonkey_scripts(
- matching_scripts.idle, QWebEngineScript.Deferred, False)
-
@pyqtSlot()
def _inject_all_greasemonkey_scripts(self):
scripts = self._greasemonkey.all_scripts()
@@ -1283,13 +1182,7 @@ class _WebEngineScripts(QObject):
page_scripts.insert(new_script)
def _inject_site_specific_quirks(self):
- """Add site-specific quirk scripts.
-
- NOTE: This isn't implemented for Qt 5.7 because of different UserScript
- semantics there. The WhatsApp Web quirk isn't needed for Qt < 5.13.
- The globalthis_quirk would be, but let's not keep such old QtWebEngine
- versions on life support.
- """
+ """Add site-specific quirk scripts."""
if not config.val.content.site_specific_quirks:
return
@@ -1305,6 +1198,9 @@ class _WebEngineScripts(QObject):
quirks.append(('globalthis_quirk',
QWebEngineScript.DocumentCreation,
QWebEngineScript.MainWorld))
+ quirks.append(('object_fromentries_quirk',
+ QWebEngineScript.DocumentCreation,
+ QWebEngineScript.MainWorld))
for filename, injection_point, world in quirks:
script = QWebEngineScript()
@@ -1380,7 +1276,6 @@ class WebEngineTab(browsertab.AbstractTab):
self.backend = usertypes.Backend.QtWebEngine
self._child_event_filter = None
self._saved_zoom = None
- self._reload_url = None # type: typing.Optional[QUrl]
self._scripts.init()
def _set_widget(self, widget):
@@ -1400,13 +1295,6 @@ class WebEngineTab(browsertab.AbstractTab):
parent=self)
self._widget.installEventFilter(self._child_event_filter)
- if qtutils.version_check('5.11', compiled=False, exact=True):
- focus_event_filter = eventfilter.FocusWorkaroundEventFilter(
- win_id=self.win_id,
- widget=self._widget,
- parent=self)
- self._widget.installEventFilter(focus_event_filter)
-
@pyqtSlot()
def _restore_zoom(self):
if sip.isdeleted(self._widget):
@@ -1417,20 +1305,17 @@ class WebEngineTab(browsertab.AbstractTab):
self.zoom.set_factor(self._saved_zoom)
self._saved_zoom = None
- def load_url(self, url, *, emit_before_load_started=True):
+ def load_url(self, url):
"""Load the given URL in this tab.
Arguments:
url: The QUrl to load.
- emit_before_load_started: If set to False, before_load_started is
- not emitted.
"""
if sip.isdeleted(self._widget):
# https://github.com/qutebrowser/qutebrowser/issues/3896
return
self._saved_zoom = self.zoom.factor()
- self._load_url_prepare(
- url, emit_before_load_started=emit_before_load_started)
+ self._load_url_prepare(url)
self._widget.load(url)
def url(self, *, requested=False):
@@ -1447,9 +1332,9 @@ class WebEngineTab(browsertab.AbstractTab):
self._widget.page().toHtml(callback)
def run_js_async(self, code, callback=None, *, world=None):
- world_id_type = typing.Union[QWebEngineScript.ScriptWorldId, int]
+ world_id_type = Union[QWebEngineScript.ScriptWorldId, int]
if world is None:
- world_id = QWebEngineScript.ApplicationWorld # type: world_id_type
+ world_id: world_id_type = QWebEngineScript.ApplicationWorld
elif isinstance(world, int):
world_id = world
if not 0 <= world_id <= qtutils.MAX_WORLD_ID:
@@ -1540,16 +1425,12 @@ class WebEngineTab(browsertab.AbstractTab):
mode=usertypes.PromptMode.user_pwd,
abort_on=[self.abort_questions], url=urlstr)
- if answer is not None:
- authenticator.setUser(answer.user)
- authenticator.setPassword(answer.password)
- else:
- try:
- sip.assign( # type: ignore[attr-defined]
- authenticator,
- QAuthenticator())
- except AttributeError:
- self._show_error_page(url, "Proxy authentication required")
+ if answer is None:
+ sip.assign(authenticator, QAuthenticator())
+ return
+
+ authenticator.setUser(answer.user)
+ authenticator.setPassword(answer.password)
@pyqtSlot(QUrl, 'QAuthenticator*')
def _on_authentication_required(self, url, authenticator):
@@ -1567,13 +1448,7 @@ class WebEngineTab(browsertab.AbstractTab):
url, authenticator, abort_on=[self.abort_questions])
if not netrc_success and answer is None:
log.network.debug("Aborting auth")
- try:
- sip.assign( # type: ignore[attr-defined]
- authenticator, QAuthenticator())
- except AttributeError:
- # WORKAROUND for
- # https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
- self._show_error_page(url, "Authentication required")
+ sip.assign(authenticator, QAuthenticator())
@pyqtSlot()
def _on_load_started(self):
@@ -1585,6 +1460,11 @@ class WebEngineTab(browsertab.AbstractTab):
super()._on_load_started()
self.data.netrc_used = False
+ @pyqtSlot('qint64')
+ def _on_renderer_process_pid_changed(self, pid):
+ log.webview.debug("Renderer process PID for tab {}: {}"
+ .format(self.tab_id, pid))
+
@pyqtSlot(QWebEnginePage.RenderProcessTerminationStatus, int)
def _on_render_process_terminated(self, status, exitcode):
"""Show an error when the renderer process terminated."""
@@ -1616,9 +1496,6 @@ class WebEngineTab(browsertab.AbstractTab):
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66643
WORKAROUND for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=882805
-
- Needs to check the page content as a WORKAROUND for
- https://bugreports.qt.io/browse/QTBUG-66661
"""
match = re.search(r'"errorCode":"([^"]*)"', html)
if match is None:
@@ -1641,7 +1518,6 @@ class WebEngineTab(browsertab.AbstractTab):
"""
super()._on_load_progress(perc)
if (perc == 100 and
- qtutils.version_check('5.10', compiled=False) and
self.load_status() != usertypes.LoadStatus.error):
self._update_load_status(ok=True)
@@ -1650,28 +1526,14 @@ class WebEngineTab(browsertab.AbstractTab):
"""QtWebEngine-specific loadFinished workarounds."""
super()._on_load_finished(ok)
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
- if qtutils.version_check('5.10', compiled=False):
- if not ok:
- self._update_load_status(ok)
- else:
+ if not ok:
+ # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
self._update_load_status(ok)
- if not ok:
self.dump_async(functools.partial(
self._error_page_workaround,
self.settings.test_attribute('content.javascript.enabled')))
- if ok and self._reload_url is not None:
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
- log.config.debug(
- "Loading {} again because of config change".format(
- self._reload_url.toDisplayString()))
- QTimer.singleShot(100, functools.partial(
- self.load_url, self._reload_url,
- emit_before_load_started=False))
- self._reload_url = None
-
@pyqtSlot(certificateerror.CertificateErrorWrapper)
def _on_ssl_errors(self, error):
url = error.url()
@@ -1689,23 +1551,17 @@ class WebEngineTab(browsertab.AbstractTab):
log.network.debug("ignore {}, URL {}, requested {}".format(
error.ignore, url, self.url(requested=True)))
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-56207
- show_cert_error = (
- not qtutils.version_check('5.9') and
- not error.ignore
- )
# WORKAROUND for https://codereview.qt-project.org/c/qt/qtwebengine/+/270556
show_non_overr_cert_error = (
not error.is_overridable() and (
# Affected Qt versions:
# 5.13 before 5.13.2
# 5.12 before 5.12.6
- # < 5.12
+ # < 5.12 (which is unsupported)
(qtutils.version_check('5.13') and
not qtutils.version_check('5.13.2')) or
(qtutils.version_check('5.12') and
- not qtutils.version_check('5.12.6')) or
- not qtutils.version_check('5.12')
+ not qtutils.version_check('5.12.6'))
)
)
@@ -1714,20 +1570,10 @@ class WebEngineTab(browsertab.AbstractTab):
# However, self.url() is not available yet and the requested URL
# might not match the URL we get from the error - so we just apply a
# heuristic here.
- if ((show_cert_error or show_non_overr_cert_error) and
+ if (show_non_overr_cert_error and
url.matches(self.data.last_navigation.url, QUrl.RemoveScheme)):
self._show_error_page(url, str(error))
- @pyqtSlot(QUrl)
- def _on_before_load_started(self, url):
- """If we know we're going to visit a URL soon, change the settings.
-
- This is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
- """
- super()._on_before_load_started(url)
- if not qtutils.version_check('5.11.1', compiled=False):
- self.settings.update_for_url(url)
-
@pyqtSlot()
def _on_print_requested(self):
"""Slot for window.print() in JS."""
@@ -1755,38 +1601,10 @@ class WebEngineTab(browsertab.AbstractTab):
def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation)
- if navigation.url == QUrl('qute://print'):
- self._on_print_requested()
- navigation.accepted = False
-
if not navigation.accepted or not navigation.is_main_frame:
return
- settings_needing_reload = {
- 'content.plugins',
- 'content.javascript.enabled',
- 'content.javascript.can_access_clipboard',
- 'content.print_element_backgrounds',
- 'input.spatial_navigation',
- }
- assert settings_needing_reload.issubset(configdata.DATA)
-
- changed = self.settings.update_for_url(navigation.url)
- reload_needed = bool(changed & settings_needing_reload)
-
- # On Qt < 5.11, we don't don't need a reload when type == link_clicked.
- # On Qt 5.11.0, we always need a reload.
- # On Qt > 5.11.0, we never need a reload:
- # https://codereview.qt-project.org/#/c/229525/1
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
- if qtutils.version_check('5.11.1', compiled=False):
- reload_needed = False
- elif not qtutils.version_check('5.11.0', exact=True, compiled=False):
- if navigation.navigation_type == navigation.Type.link_clicked:
- reload_needed = False
-
- if reload_needed:
- self._reload_url = navigation.url
+ self.settings.update_for_url(navigation.url)
def _on_select_client_certificate(self, selection):
"""Handle client certificates.
@@ -1833,9 +1651,7 @@ class WebEngineTab(browsertab.AbstractTab):
self._on_proxy_authentication_required)
page.contentsSizeChanged.connect(self.contents_size_changed)
page.navigation_request.connect(self._on_navigation_request)
-
- if qtutils.version_check('5.12'):
- page.printRequested.connect(self._on_print_requested)
+ page.printRequested.connect(self._on_print_requested)
try:
# pylint: disable=unused-import
@@ -1857,11 +1673,14 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished)
- self.before_load_started.connect(self._on_before_load_started)
- self.shutting_down.connect(
- self.abort_questions) # type: ignore[arg-type]
- self.load_started.connect(
- self.abort_questions) # type: ignore[arg-type]
+ try:
+ page.renderProcessPidChanged.connect(self._on_renderer_process_pid_changed)
+ except AttributeError:
+ # Added in Qt 5.15.0
+ pass
+
+ self.shutting_down.connect(self.abort_questions)
+ self.load_started.connect(self.abort_questions)
# pylint: disable=protected-access
self.audio._connect_signals()
diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py
index 40ac12f11..23af73e59 100644
--- a/qutebrowser/browser/webengine/webview.py
+++ b/qutebrowser/browser/webengine/webview.py
@@ -19,19 +19,15 @@
"""The main browser widget for QtWebEngine."""
-import typing
-from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION
+from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtGui import QPalette
-from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import webenginesettings, certificateerror
from qutebrowser.config import config
-from qutebrowser.utils import log, debug, usertypes, qtutils
-from qutebrowser.misc import miscwidgets, objects
-from qutebrowser.qt import sip
+from qutebrowser.utils import log, debug, usertypes
class WebEngineView(QWebEngineView):
@@ -54,42 +50,9 @@ class WebEngineView(QWebEngineView):
parent=self)
self.setPage(page)
- if qtutils.version_check('5.11.0', compiled=False, exact=True):
- # Set a PseudoLayout as a WORKAROUND for
- # https://bugreports.qt.io/browse/QTBUG-68224
- # and other related issues. (Fixed in Qt 5.11.1)
- sip.delete(self.layout())
- self._layout = miscwidgets.PseudoLayout(self)
-
def render_widget(self):
- """Get the RenderWidgetHostViewQt for this view.
-
- Normally, this would always be the focusProxy().
- However, it sometimes isn't, so we use this as a WORKAROUND for
- https://bugreports.qt.io/browse/QTBUG-68727
-
- The above bug got introduced in Qt 5.11.0 and fixed in 5.12.0.
- """
- proxy = self.focusProxy() # type: typing.Optional[QWidget]
-
- if 'lost-focusproxy' in objects.debug_flags:
- proxy = None
-
- if (proxy is not None or
- not qtutils.version_check('5.11', compiled=False) or
- qtutils.version_check('5.12', compiled=False)):
- return proxy
-
- # We don't want e.g. a QMenu.
- rwhv_class = 'QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget'
- children = [c for c in self.findChildren(QWidget)
- if c.isVisible() and c.inherits(rwhv_class)]
-
- if children:
- log.webview.debug("Found possibly lost focusProxy: {}"
- .format(children))
-
- return children[-1] if children else None
+ """Get the RenderWidgetHostViewQt for this view."""
+ return self.focusProxy()
def shutdown(self):
self.page().shutdown()
@@ -207,42 +170,29 @@ class WebEnginePage(QWebEnginePage):
"""Override javaScriptConfirm to use qutebrowser prompts."""
if self._is_shutting_down:
return False
- escape_msg = qtutils.version_check('5.11', compiled=False)
try:
- return shared.javascript_confirm(url, js_msg,
- abort_on=[self.loadStarted,
- self.shutting_down],
- escape_msg=escape_msg)
+ return shared.javascript_confirm(
+ url, js_msg, abort_on=[self.loadStarted, self.shutting_down])
except shared.CallSuper:
return super().javaScriptConfirm(url, js_msg)
- if PYQT_VERSION > 0x050700:
- # WORKAROUND
- # Can't override javaScriptPrompt with older PyQt versions
- # https://www.riverbankcomputing.com/pipermail/pyqt/2016-November/038293.html
- def javaScriptPrompt(self, url, js_msg, default):
- """Override javaScriptPrompt to use qutebrowser prompts."""
- escape_msg = qtutils.version_check('5.11', compiled=False)
- if self._is_shutting_down:
- return (False, "")
- try:
- return shared.javascript_prompt(url, js_msg, default,
- abort_on=[self.loadStarted,
- self.shutting_down],
- escape_msg=escape_msg)
- except shared.CallSuper:
- return super().javaScriptPrompt(url, js_msg, default)
+ def javaScriptPrompt(self, url, js_msg, default):
+ """Override javaScriptPrompt to use qutebrowser prompts."""
+ if self._is_shutting_down:
+ return (False, "")
+ try:
+ return shared.javascript_prompt(
+ url, js_msg, default, abort_on=[self.loadStarted, self.shutting_down])
+ except shared.CallSuper:
+ return super().javaScriptPrompt(url, js_msg, default)
def javaScriptAlert(self, url, js_msg):
"""Override javaScriptAlert to use qutebrowser prompts."""
if self._is_shutting_down:
return
- escape_msg = qtutils.version_check('5.11', compiled=False)
try:
- shared.javascript_alert(url, js_msg,
- abort_on=[self.loadStarted,
- self.shutting_down],
- escape_msg=escape_msg)
+ shared.javascript_alert(
+ url, js_msg, abort_on=[self.loadStarted, self.shutting_down])
except shared.CallSuper:
super().javaScriptAlert(url, js_msg)
diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py
index 7202ebd23..79c4e9e82 100644
--- a/qutebrowser/browser/webkit/cache.py
+++ b/qutebrowser/browser/webkit/cache.py
@@ -19,16 +19,16 @@
"""HTTP network cache."""
-import typing
+from typing import cast
import os.path
from PyQt5.QtNetwork import QNetworkDiskCache
from qutebrowser.config import config
-from qutebrowser.utils import utils, qtutils, standarddir
+from qutebrowser.utils import utils, standarddir
-diskcache = typing.cast('DiskCache', None)
+diskcache = cast('DiskCache', None)
class DiskCache(QNetworkDiskCache):
@@ -52,9 +52,6 @@ class DiskCache(QNetworkDiskCache):
size = config.val.content.cache.size
if size is None:
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909
- if not qtutils.version_check('5.9', compiled=False):
- size = 0 # pragma: no cover
self.setMaximumCacheSize(size)
diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py
index 4b2070f1d..9cc28cf69 100644
--- a/qutebrowser/browser/webkit/cookies.py
+++ b/qutebrowser/browser/webkit/cookies.py
@@ -19,7 +19,7 @@
"""Handling of HTTP cookies."""
-import typing
+from typing import Sequence
from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar
from PyQt5.QtCore import pyqtSignal, QDateTime
@@ -93,7 +93,7 @@ class CookieJar(RAMCookieJar):
def parse_cookies(self):
"""Parse cookies from lineparser and store them."""
- cookies = [] # type: typing.Sequence[QNetworkCookie]
+ cookies: Sequence[QNetworkCookie] = []
for line in self._lineparser:
line_cookies = QNetworkCookie.parseCookies(line)
cookies += line_cookies # type: ignore[operator]
diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py
index a045e10f2..794e1b73b 100644
--- a/qutebrowser/browser/webkit/mhtml.py
+++ b/qutebrowser/browser/webkit/mhtml.py
@@ -33,7 +33,7 @@ import email.encoders
import email.mime.multipart
import email.message
import quopri
-import typing
+from typing import MutableMapping, Set, Tuple
import attr
from PyQt5.QtCore import QUrl
@@ -62,7 +62,7 @@ _CSS_URL_PATTERNS = [re.compile(x) for x in [
]]
-def _get_css_imports_regex(data):
+def _get_css_imports(data):
"""Return all assets that are referenced in the given CSS document.
The returned URLs are relative to the stylesheet's URL.
@@ -79,58 +79,6 @@ def _get_css_imports_regex(data):
return urls
-def _get_css_imports_cssutils(data, inline=False):
- """Return all assets that are referenced in the given CSS document.
-
- The returned URLs are relative to the stylesheet's URL.
-
- Args:
- data: The content of the stylesheet to scan as string.
- inline: True if the argument is an inline HTML style attribute.
- """
- try:
- import cssutils
- except (ImportError, re.error):
- # Catching re.error because cssutils in earlier releases (<= 1.0) is
- # broken on Python 3.5
- # See https://bitbucket.org/cthedot/cssutils/issues/52
- return None
-
- # We don't care about invalid CSS data, this will only litter the log
- # output with CSS errors
- parser = cssutils.CSSParser(loglevel=100,
- fetcher=lambda url: (None, ""), validate=False)
- if not inline:
- sheet = parser.parseString(data)
- return list(cssutils.getUrls(sheet))
- else:
- urls = []
- declaration = parser.parseStyle(data)
- # prop = background, color, margin, ...
- for prop in declaration:
- # value = red, 10px, url(foobar), ...
- for value in prop.propertyValue:
- if isinstance(value, cssutils.css.URIValue):
- if value.uri:
- urls.append(value.uri)
- return urls
-
-
-def _get_css_imports(data, inline=False):
- """Return all assets that are referenced in the given CSS document.
-
- The returned URLs are relative to the stylesheet's URL.
-
- Args:
- data: The content of the stylesheet to scan as string.
- inline: True if the argument is an inline HTML style attribute.
- """
- imports = _get_css_imports_cssutils(data, inline)
- if imports is None:
- imports = _get_css_imports_regex(data)
- return imports
-
-
def _check_rel(element):
"""Return true if the element's rel attribute fits our criteria.
@@ -189,7 +137,7 @@ class MHTMLWriter:
self.root_content = root_content
self.content_location = content_location
self.content_type = content_type
- self._files = {} # type: typing.MutableMapping[QUrl, _File]
+ self._files: MutableMapping[QUrl, _File] = {}
def add_file(self, location, content, content_type=None,
transfer_encoding=E_QUOPRI):
@@ -244,8 +192,7 @@ class MHTMLWriter:
return msg
-_PendingDownloadType = typing.Set[
- typing.Tuple[QUrl, downloads.AbstractDownloadItem]]
+_PendingDownloadType = Set[Tuple[QUrl, downloads.AbstractDownloadItem]]
class _Downloader:
@@ -268,7 +215,7 @@ class _Downloader:
self.target = target
self.writer = None
self.loaded_urls = {tab.url()}
- self.pending_downloads = set() # type: _PendingDownloadType
+ self.pending_downloads: _PendingDownloadType = set()
self._finished_file = False
self._used = False
@@ -332,7 +279,7 @@ class _Downloader:
for element in web_frame.findAllElements('[style]'):
element = webkitelem.WebKitElement(element, tab=self.tab)
style = element['style']
- for element_url in _get_css_imports(style, inline=True):
+ for element_url in _get_css_imports(style):
self._fetch_url(web_url.resolved(QUrl(element_url)))
# Shortcut if no assets need to be downloaded, otherwise the file would
diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py
index 1def7ad44..3e393680c 100644
--- a/qutebrowser/browser/webkit/network/networkmanager.py
+++ b/qutebrowser/browser/webkit/network/networkmanager.py
@@ -21,7 +21,7 @@
import collections
import html
-import typing
+from typing import TYPE_CHECKING, Dict, MutableMapping, Optional, Sequence
import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
@@ -40,12 +40,12 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme)
from qutebrowser.misc import objects
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.mainwindow import prompt
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
-_proxy_auth_cache = {} # type: typing.Dict[ProxyId, prompt.AuthInfo]
+_proxy_auth_cache: Dict['ProxyId', 'prompt.AuthInfo'] = {}
@attr.s(frozen=True)
@@ -123,8 +123,7 @@ def init():
QSslSocket.setDefaultCiphers(good_ciphers)
-_SavedErrorsType = typing.MutableMapping[urlutils.HostTupleType,
- typing.Sequence[QSslError]]
+_SavedErrorsType = MutableMapping[urlutils.HostTupleType, Sequence[QSslError]]
class NetworkManager(QNetworkAccessManager):
@@ -156,10 +155,7 @@ class NetworkManager(QNetworkAccessManager):
def __init__(self, *, win_id, tab_id, private, parent=None):
log.init.debug("Initializing NetworkManager")
- with log.disable_qt_msghandler():
- # WORKAROUND for a hang when a message is printed - See:
- # http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html
- super().__init__(parent)
+ super().__init__(parent)
log.init.debug("NetworkManager init done")
self.adopted_downloads = 0
self._win_id = win_id
@@ -173,10 +169,8 @@ class NetworkManager(QNetworkAccessManager):
self._set_cache()
self.sslErrors.connect( # type: ignore[attr-defined]
self.on_ssl_errors)
- self._rejected_ssl_errors = collections.defaultdict(
- list) # type: _SavedErrorsType
- self._accepted_ssl_errors = collections.defaultdict(
- list) # type: _SavedErrorsType
+ self._rejected_ssl_errors: _SavedErrorsType = collections.defaultdict(list)
+ self._accepted_ssl_errors: _SavedErrorsType = collections.defaultdict(list)
self.authenticationRequired.connect( # type: ignore[attr-defined]
self.on_authentication_required)
self.proxyAuthenticationRequired.connect( # type: ignore[attr-defined]
@@ -241,8 +235,8 @@ class NetworkManager(QNetworkAccessManager):
log.network.debug("Certificate errors: {!r}".format(
' / '.join(str(err) for err in errors)))
try:
- host_tpl = urlutils.host_tuple(
- reply.url()) # type: typing.Optional[urlutils.HostTupleType]
+ host_tpl: Optional[urlutils.HostTupleType] = urlutils.host_tuple(
+ reply.url())
except ValueError:
host_tpl = None
is_accepted = False
@@ -371,13 +365,6 @@ class NetworkManager(QNetworkAccessManager):
# https://www.playstation.com/ for example.
pass
- # WORKAROUND for:
- # http://www.riverbankcomputing.com/pipermail/pyqt/2014-September/034806.html
- #
- # By returning False, we provoke a TypeError because of a wrong return
- # type, which does *not* trigger a segfault but invoke our return handler
- # immediately.
- @utils.prevent_exceptions(False)
def createRequest(self, op, req, outgoing_data):
"""Return a new QNetworkReply object.
diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py
index f293edacd..f0673036c 100644
--- a/qutebrowser/browser/webkit/tabhistory.py
+++ b/qutebrowser/browser/webkit/tabhistory.py
@@ -19,7 +19,7 @@
"""Utilities related to QWebHistory."""
-import typing
+from typing import Any, List, Mapping
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
@@ -81,7 +81,7 @@ def serialize(items):
"""
data = QByteArray()
stream = QDataStream(data, QIODevice.ReadWrite)
- user_data = [] # type: typing.List[typing.Mapping[str, typing.Any]]
+ user_data: List[Mapping[str, Any]] = []
current_idx = None
diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py
index 9f58031de..2f3562b7f 100644
--- a/qutebrowser/browser/webkit/webkitelem.py
+++ b/qutebrowser/browser/webkit/webkitelem.py
@@ -19,7 +19,7 @@
"""QtWebKit specific part of the web element API."""
-import typing
+from typing import cast, TYPE_CHECKING, Iterator, List, Optional, Set
from PyQt5.QtCore import QRect, Qt
from PyQt5.QtWebKit import QWebElement, QWebSettings
@@ -29,7 +29,7 @@ from qutebrowser.config import config
from qutebrowser.utils import log, utils, javascript, usertypes
from qutebrowser.browser import webelem
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.browser.webkit import webkittab
@@ -42,6 +42,8 @@ class WebKitElement(webelem.AbstractWebElement):
"""A wrapper around a QWebElement."""
+ _tab: 'webkittab.WebKitTab'
+
def __init__(self, elem: QWebElement, tab: 'webkittab.WebKitTab') -> None:
super().__init__(tab)
if isinstance(elem, self.__class__):
@@ -80,7 +82,7 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished()
return self._elem.hasAttribute(key)
- def __iter__(self) -> typing.Iterator[str]:
+ def __iter__(self) -> Iterator[str]:
self._check_vanished()
yield from self._elem.attributeNames()
@@ -101,9 +103,9 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished()
return self._elem.geometry()
- def classes(self) -> typing.List[str]:
+ def classes(self) -> Set[str]:
self._check_vanished()
- return self._elem.classes()
+ return set(self._elem.classes())
def tag_name(self) -> str:
"""Get the tag name for the current element."""
@@ -174,21 +176,16 @@ class WebKitElement(webelem.AbstractWebElement):
this.dispatchEvent(event);
""".format(javascript.to_js(text)))
- def _parent(self) -> typing.Optional['WebKitElement']:
+ def _parent(self) -> Optional['WebKitElement']:
"""Get the parent element of this element."""
self._check_vanished()
- elem = typing.cast(typing.Optional[QWebElement],
- self._elem.parent())
+ elem = cast(Optional[QWebElement], self._elem.parent())
if elem is None or elem.isNull():
return None
- if typing.TYPE_CHECKING:
- # pylint: disable=used-before-assignment
- assert isinstance(self._tab, webkittab.WebKitTab)
-
return WebKitElement(elem, tab=self._tab)
- def _rect_on_view_js(self) -> typing.Optional[QRect]:
+ def _rect_on_view_js(self) -> Optional[QRect]:
"""Javascript implementation for rect_on_view."""
# FIXME:qtwebengine maybe we can reuse this?
rects = self._elem.evaluateJavaScript("this.getClientRects()")
@@ -217,29 +214,32 @@ class WebKitElement(webelem.AbstractWebElement):
height *= zoom
rect = QRect(int(rect["left"]), int(rect["top"]),
int(width), int(height))
- frame = self._elem.webFrame()
+
+ frame = cast(Optional[QWebFrame], self._elem.webFrame())
while frame is not None:
# Translate to parent frames' position (scroll position
# is taken care of inside getClientRects)
rect.translate(frame.geometry().topLeft())
frame = frame.parentFrame()
+
return rect
return None
- def _rect_on_view_python(self,
- elem_geometry: typing.Optional[QRect]) -> QRect:
+ def _rect_on_view_python(self, elem_geometry: Optional[QRect]) -> QRect:
"""Python implementation for rect_on_view."""
if elem_geometry is None:
geometry = self._elem.geometry()
else:
geometry = elem_geometry
- frame = self._elem.webFrame()
rect = QRect(geometry)
+
+ frame = cast(Optional[QWebFrame], self._elem.webFrame())
while frame is not None:
rect.translate(frame.geometry().topLeft())
rect.translate(frame.scrollPosition() * -1)
- frame = frame.parentFrame()
+ frame = cast(Optional[QWebFrame], frame.parentFrame())
+
return rect
def rect_on_view(self, *, elem_geometry: QRect = None,
@@ -332,7 +332,7 @@ class WebKitElement(webelem.AbstractWebElement):
return all([visible_on_screen, visible_in_frame])
def remove_blank_target(self) -> None:
- elem = self # type: typing.Optional[WebKitElement]
+ elem: Optional[WebKitElement] = self
for _ in range(5):
if elem is None:
break
@@ -377,7 +377,7 @@ class WebKitElement(webelem.AbstractWebElement):
super()._click_fake_event(click_target)
-def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]:
+def get_child_frames(startframe: QWebFrame) -> List[QWebFrame]:
"""Get all children recursively of a given QWebFrame.
Loosely based on http://blog.nextgenetics.net/?e=64
@@ -391,7 +391,7 @@ def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]:
results = []
frames = [startframe]
while frames:
- new_frames = [] # type: typing.List[QWebFrame]
+ new_frames: List[QWebFrame] = []
for frame in frames:
results.append(frame)
new_frames += frame.childFrames()
diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py
index 0db1a738d..db36dc899 100644
--- a/qutebrowser/browser/webkit/webkitsettings.py
+++ b/qutebrowser/browser/webkit/webkitsettings.py
@@ -24,7 +24,7 @@ Module attributes:
constants.
"""
-import typing
+from typing import cast
import os.path
from PyQt5.QtCore import QUrl
@@ -39,7 +39,7 @@ from qutebrowser.browser import shared
# The global WebKitSettings object
-global_settings = typing.cast('WebKitSettings', None)
+global_settings = cast('WebKitSettings', None)
parsed_user_agent = None
@@ -172,7 +172,7 @@ def _init_user_agent():
parsed_user_agent = websettings.UserAgent.parse(ua)
-def init(_args):
+def init():
"""Initialize the global QWebSettings."""
cache_path = standarddir.cache()
data_path = standarddir.data()
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index cad9badee..69773fa57 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -22,12 +22,13 @@
import re
import functools
import xml.etree.ElementTree
+from typing import cast, Iterable
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
-from PyQt5.QtWebKit import QWebSettings
+from PyQt5.QtWebKit import QWebSettings, QWebHistory, QWebElement
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab, shared
@@ -81,9 +82,6 @@ class WebKitPrinting(browsertab.AbstractPrinting):
def check_pdf_support(self):
pass
- def check_printer_support(self):
- pass
-
def check_preview_support(self):
pass
@@ -200,8 +198,7 @@ class WebKitCaret(browsertab.AbstractCaret):
tab: 'WebKitTab',
mode_manager: modeman.ModeManager,
parent: QWidget = None) -> None:
- super().__init__(mode_manager, parent)
- self._tab = tab
+ super().__init__(tab, mode_manager, parent)
self._selection_state = browsertab.SelectionState.none
@pyqtSlot(usertypes.KeyMode)
@@ -622,6 +619,10 @@ class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""History-related methods which are not part of the extension API."""
+ def __init__(self, tab: 'WebKitTab') -> None:
+ self._tab = tab
+ self._history = cast(QWebHistory, None)
+
def serialize(self):
return qtutils.serialize(self._history)
@@ -636,6 +637,7 @@ class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate):
qtutils.deserialize_stream(stream, self._history)
for i, data in enumerate(user_data):
self._history.itemAt(i).setUserData(data)
+
cur_data = self._history.currentItem().userData()
if cur_data is not None:
if 'zoom' in cur_data:
@@ -687,9 +689,7 @@ class WebKitElements(browsertab.AbstractElements):
"""QtWebKit implemementations related to elements on the page."""
- def __init__(self, tab: 'WebKitTab') -> None:
- super().__init__()
- self._tab = tab
+ _tab: 'WebKitTab'
def find_css(self, selector, callback, error_cb, *, only_visible=False):
utils.unused(error_cb)
@@ -700,7 +700,8 @@ class WebKitElements(browsertab.AbstractElements):
elems = []
frames = webkitelem.get_child_frames(mainframe)
for f in frames:
- for elem in f.findAllElements(selector):
+ frame_elems = cast(Iterable[QWebElement], f.findAllElements(selector))
+ for elem in frame_elems:
elems.append(webkitelem.WebKitElement(elem, tab=self._tab))
if only_visible:
@@ -846,9 +847,8 @@ class WebKitTab(browsertab.AbstractTab):
settings = widget.settings()
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
- def load_url(self, url, *, emit_before_load_started=True):
- self._load_url_prepare(
- url, emit_before_load_started=emit_before_load_started)
+ def load_url(self, url):
+ self._load_url_prepare(url)
self._widget.load(url)
def url(self, *, requested=False):
diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py
index 9055bff24..c20acb369 100644
--- a/qutebrowser/browser/webkit/webpage.py
+++ b/qutebrowser/browser/webkit/webpage.py
@@ -21,7 +21,7 @@
import html
import functools
-import typing
+from typing import cast
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices
@@ -353,11 +353,11 @@ class BrowserPage(QWebPage):
self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser)
- url = frame.url().adjusted(typing.cast(QUrl.FormattingOptions,
- QUrl.RemoveUserInfo |
- QUrl.RemovePath |
- QUrl.RemoveQuery |
- QUrl.RemoveFragment))
+ url = frame.url().adjusted(cast(QUrl.FormattingOptions,
+ QUrl.RemoveUserInfo |
+ QUrl.RemovePath |
+ QUrl.RemoveQuery |
+ QUrl.RemoveFragment))
question = shared.feature_permission(
url=url,
option=options[feature], msg=messages[feature],
@@ -365,7 +365,7 @@ class BrowserPage(QWebPage):
abort_on=[self.shutting_down, self.loadStarted])
if question is not None:
- self.featurePermissionRequestCanceled.connect( # type: ignore
+ self.featurePermissionRequestCanceled.connect( # type: ignore[attr-defined]
functools.partial(self._on_feature_permission_cancelled,
question, frame, feature))
diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py
index 6706848dd..428e66744 100644
--- a/qutebrowser/browser/webkit/webview.py
+++ b/qutebrowser/browser/webkit/webview.py
@@ -20,7 +20,6 @@
"""The main browser widgets."""
from PyQt5.QtCore import pyqtSignal, Qt, QUrl
-from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
@@ -62,10 +61,6 @@ class WebView(QWebView):
def __init__(self, *, win_id, tab_id, tab, private, parent=None):
super().__init__(parent)
- if utils.is_mac:
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
- # See https://github.com/qutebrowser/qutebrowser/issues/462
- self.setStyle(QStyleFactory.create('Fusion'))
# FIXME:qtwebengine this is only used to set the zoom factor from
# the QWebPage - we should get rid of it somehow (signals?)
self.tab = tab
@@ -181,9 +176,9 @@ class WebView(QWebView):
This is not needed for QtWebEngine, so it's in here.
"""
menu = self.page().createStandardContextMenu()
- self.shutting_down.connect(menu.close) # type: ignore[arg-type]
+ self.shutting_down.connect(menu.close)
mm = modeman.instance(self.win_id)
- mm.entered.connect(menu.close) # type: ignore[arg-type]
+ mm.entered.connect(menu.close)
menu.exec_(e.globalPos())
def showEvent(self, e):
diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py
index 2672fcd68..61b44d555 100644
--- a/qutebrowser/commands/command.py
+++ b/qutebrowser/commands/command.py
@@ -23,6 +23,7 @@ import inspect
import collections
import traceback
import typing
+from typing import Any, MutableMapping, MutableSequence, Tuple, Union
import attr
@@ -116,13 +117,11 @@ class Command:
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
default=argparser.SUPPRESS, nargs=0,
help=argparser.SUPPRESS)
- self.opt_args = collections.OrderedDict(
- ) # type: typing.MutableMapping[str, typing.Tuple[str, str]]
+ self.opt_args: MutableMapping[str, Tuple[str, str]] = collections.OrderedDict()
self.namespace = None
self._count = None
- self.pos_args = [
- ] # type: typing.MutableSequence[typing.Tuple[str, str]]
- self.flags_with_args = [] # type: typing.MutableSequence[str]
+ self.pos_args: MutableSequence[Tuple[str, str]] = []
+ self.flags_with_args: MutableSequence[str] = []
self._has_vararg = False
# This is checked by future @cmdutils.argument calls so they fail
@@ -406,22 +405,19 @@ class Command:
raise TypeError("{}: Legacy tuple type annotation!".format(
self.name))
- if hasattr(typing, 'UnionMeta'):
- # Python 3.5.2
- # pylint: disable=no-member,useless-suppression
- is_union = isinstance(
- typ, typing.UnionMeta) # type: ignore[attr-defined]
- else:
- is_union = getattr(typ, '__origin__', None) is typing.Union
+ try:
+ origin = typing.get_origin(typ) # type: ignore[attr-defined]
+ except AttributeError:
+ # typing.get_origin was added in Python 3.8
+ origin = getattr(typ, '__origin__', None)
- if is_union:
- # this is... slightly evil, I know
+ if origin is Union:
try:
- types = list(typ.__args__)
+ types = list(typing.get_args(typ)) # type: ignore[attr-defined]
except AttributeError:
- # Python 3.5.2
- types = list(typ.__union_params__)
- # pylint: enable=no-member,useless-suppression
+ # typing.get_args was added in Python 3.8
+ types = list(typ.__args__)
+
if param.default is not inspect.Parameter.empty:
types.append(type(param.default))
choices = self.get_arg_info(param).choices
@@ -497,8 +493,8 @@ class Command:
Return:
An (args, kwargs) tuple.
"""
- args = [] # type: typing.Any
- kwargs = {} # type: typing.MutableMapping[str, typing.Any]
+ args: Any = []
+ kwargs: MutableMapping[str, Any] = {}
signature = inspect.signature(self.handler)
for i, param in enumerate(signature.parameters.values()):
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
index 76ae1d64f..c195a8be9 100644
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -21,8 +21,8 @@
import traceback
import re
-import typing
import contextlib
+from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping
import attr
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
@@ -34,9 +34,9 @@ from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split, objects
from qutebrowser.keyinput import macros, modeman
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.mainwindow import tabbedbrowser
-_ReplacementFunction = typing.Callable[['tabbedbrowser.TabbedBrowser'], str]
+_ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str]
last_command = {}
@@ -64,9 +64,9 @@ def _url(tabbed_browser):
raise cmdutils.CommandError(msg)
-def _init_variable_replacements() -> typing.Mapping[str, _ReplacementFunction]:
+def _init_variable_replacements() -> Mapping[str, _ReplacementFunction]:
"""Return a dict from variable replacements to fns processing them."""
- replacements = {
+ replacements: Dict[str, _ReplacementFunction] = {
'url': lambda tb: _url(tb).toString(
QUrl.FullyEncoded | QUrl.RemovePassword),
'url:pretty': lambda tb: _url(tb).toString(
@@ -88,7 +88,7 @@ def _init_variable_replacements() -> typing.Mapping[str, _ReplacementFunction]:
'title': lambda tb: tb.widget.page_title(tb.widget.currentIndex()),
'clipboard': lambda _: utils.get_clipboard(),
'primary': lambda _: utils.get_clipboard(selection=True),
- } # type: typing.Dict[str, _ReplacementFunction]
+ }
for key in list(replacements):
modified_key = '{' + key + '}'
@@ -108,7 +108,7 @@ def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
- values = {} # type: typing.MutableMapping[str, str]
+ values: MutableMapping[str, str] = {}
args = []
def repl_cb(matchobj):
@@ -332,7 +332,7 @@ class CommandRunner(AbstractCommandRunner):
self._win_id = win_id
@contextlib.contextmanager
- def _handle_error(self, safely: bool) -> typing.Iterator[None]:
+ def _handle_error(self, safely: bool) -> Iterator[None]:
"""Show exceptions as errors if safely=True is given."""
try:
yield
diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py
index 485161600..6d2c2f147 100644
--- a/qutebrowser/commands/userscripts.py
+++ b/qutebrowser/commands/userscripts.py
@@ -22,7 +22,7 @@
import os
import os.path
import tempfile
-import typing
+from typing import cast, Any, MutableMapping, Tuple
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
@@ -60,7 +60,7 @@ class _QtFIFOReader(QObject):
fd = os.open(filepath, os.O_RDWR | os.O_NONBLOCK)
# pylint: enable=no-member,useless-suppression
self._fifo = os.fdopen(fd, 'r')
- self._notifier = QSocketNotifier(typing.cast(sip.voidptr, fd),
+ self._notifier = QSocketNotifier(cast(sip.voidptr, fd),
QSocketNotifier.Read, self)
self._notifier.activated.connect( # type: ignore[attr-defined]
self.read_line)
@@ -117,10 +117,10 @@ class _BaseUserscriptRunner(QObject):
self._cleaned_up = False
self._filepath = None
self._proc = None
- self._env = {} # type: typing.MutableMapping[str, str]
+ self._env: MutableMapping[str, str] = {}
self._text_stored = False
self._html_stored = False
- self._args = () # type: typing.Tuple[typing.Any, ...]
+ self._args: Tuple[Any, ...] = ()
self._kwargs = {}
def store_text(self, text):
@@ -267,7 +267,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
return
self._reader = _QtFIFOReader(self._filepath)
- self._reader.got_line.connect(self.got_cmd) # type: ignore[arg-type]
+ self._reader.got_line.connect(self.got_cmd)
@pyqtSlot()
def on_proc_finished(self):
@@ -395,6 +395,7 @@ def _lookup_path(cmd):
directories = [
os.path.join(standarddir.data(), "userscripts"),
os.path.join(standarddir.data(system=True), "userscripts"),
+ os.path.join(standarddir.config(), "userscripts"),
]
for directory in directories:
cmd_path = os.path.join(directory, cmd)
@@ -426,7 +427,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False,
commandrunner = runners.CommandRunner(win_id, parent=tb)
if utils.is_posix:
- runner = _POSIXUserscriptRunner(tb) # type: _BaseUserscriptRunner
+ runner: _BaseUserscriptRunner = _POSIXUserscriptRunner(tb)
elif utils.is_windows: # pragma: no cover
runner = _WindowsUserscriptRunner(tb)
else: # pragma: no cover
diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py
index a8e58b8a2..4e1290f82 100644
--- a/qutebrowser/completion/completiondelegate.py
+++ b/qutebrowser/completion/completiondelegate.py
@@ -47,6 +47,7 @@ class _Highlighter(QSyntaxHighlighter):
self._expression = QRegularExpression(
pat, QRegularExpression.CaseInsensitiveOption
)
+ qtutils.ensure_valid(self._expression)
def highlightBlock(self, text):
"""Override highlightBlock for custom highlighting."""
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
index 50d5bdf62..86de688a0 100644
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -23,16 +23,16 @@ Defines a CompletionView which uses CompletionFiterModel and CompletionModel
subclasses to provide completions.
"""
-import typing
+from typing import TYPE_CHECKING, Optional
from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory, QWidget
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config, stylesheet
from qutebrowser.completion import completiondelegate
-from qutebrowser.utils import utils, usertypes, debug, log
+from qutebrowser.utils import utils, usertypes, debug, log, qtutils
from qutebrowser.api import cmdutils
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.mainwindow.statusbar import command
@@ -115,7 +115,7 @@ class CompletionView(QTreeView):
win_id: int,
parent: QWidget = None) -> None:
super().__init__(parent)
- self.pattern = None # type: typing.Optional[str]
+ self.pattern: Optional[str] = None
self._win_id = win_id
self._cmd = cmd
self._active = False
@@ -223,8 +223,9 @@ class CompletionView(QTreeView):
return model.last_item() if upwards else model.first_item()
# Find height of each CompletionView element
- element_height = self.visualRect(idx).height()
- page_length = self.height() // element_height
+ rect = self.visualRect(idx)
+ qtutils.ensure_valid(rect)
+ page_length = self.height() // rect.height()
# Skip one pageful, except leave one old line visible
offset = -(page_length - 1) if upwards else page_length - 1
@@ -330,7 +331,8 @@ class CompletionView(QTreeView):
QItemSelectionModel.Rows)
# if the last item is focused, try to fetch more
- if idx.row() == self.model().rowCount(idx.parent()) - 1:
+ next_idx = self.indexBelow(idx)
+ if not self.visualRect(next_idx).isValid():
self.expandAll()
count = self.model().count()
diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py
index 1bd2a808f..d992a44f4 100644
--- a/qutebrowser/completion/models/completionmodel.py
+++ b/qutebrowser/completion/models/completionmodel.py
@@ -19,7 +19,7 @@
"""A model that proxies access to one or more completion categories."""
-import typing
+from typing import MutableSequence
from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel
@@ -43,8 +43,7 @@ class CompletionModel(QAbstractItemModel):
def __init__(self, *, column_widths=(30, 70, 0), parent=None):
super().__init__(parent)
self.column_widths = column_widths
- self._categories = [
- ] # type: typing.MutableSequence[QAbstractItemModel]
+ self._categories: MutableSequence[QAbstractItemModel] = []
def _cat_from_idx(self, index):
"""Return the category pointed to by the given index.
@@ -180,16 +179,10 @@ class CompletionModel(QAbstractItemModel):
pattern: The filter pattern to set.
"""
log.completion.debug("Setting completion pattern '{}'".format(pattern))
- # WORKAROUND:
- # layoutChanged is broken in PyQt 5.7.1, so we must use metaObject
- # https://www.riverbankcomputing.com/pipermail/pyqt/2017-January/038483.html
- meta = self.metaObject()
- meta.invokeMethod(self, # type: ignore[misc, call-overload]
- "layoutAboutToBeChanged")
+ self.layoutAboutToBeChanged.emit() # type: ignore[attr-defined]
for cat in self._categories:
cat.set_pattern(pattern)
- meta.invokeMethod(self, # type: ignore[misc, call-overload]
- "layoutChanged")
+ self.layoutChanged.emit() # type: ignore[attr-defined]
def first_item(self):
"""Return the index of the first child (non-category) in the model."""
diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py
index 464caa19e..e7ccd3505 100644
--- a/qutebrowser/completion/models/histcategory.py
+++ b/qutebrowser/completion/models/histcategory.py
@@ -19,7 +19,7 @@
"""A completion category that queries the SQL history store."""
-import typing
+from typing import Optional
from PyQt5.QtSql import QSqlQueryModel
from PyQt5.QtWidgets import QWidget
@@ -40,12 +40,12 @@ class HistoryCategory(QSqlQueryModel):
"""Create a new History completion category."""
super().__init__(parent=parent)
self.name = "History"
- self._query = None # type: typing.Optional[sql.Query]
+ self._query: Optional[sql.Query] = None
# advertise that this model filters by URL and title
self.columns_to_filter = [0, 1]
self.delete_func = delete_func
- self._empty_prefix = None # type: typing.Optional[str]
+ self._empty_prefix: Optional[str] = None
def _atime_expr(self):
"""If max_items is set, return an expression to limit the query."""
diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py
index 6995071ed..d4193b6d8 100644
--- a/qutebrowser/completion/models/listcategory.py
+++ b/qutebrowser/completion/models/listcategory.py
@@ -20,9 +20,9 @@
"""Completion category that uses a list of tuples as a data source."""
import re
-import typing
+from typing import Iterable, Tuple
-from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp
+from PyQt5.QtCore import QSortFilterProxyModel, QRegularExpression
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QWidget
@@ -36,7 +36,7 @@ class ListCategory(QSortFilterProxyModel):
def __init__(self,
name: str,
- items: typing.Iterable[typing.Tuple[str, ...]],
+ items: Iterable[Tuple[str, ...]],
sort: bool = True,
delete_func: util.DeleteFuncType = None,
parent: QWidget = None):
@@ -63,8 +63,9 @@ class ListCategory(QSortFilterProxyModel):
val = re.sub(r' +', r' ', val) # See #1919
val = re.escape(val)
val = val.replace(r'\ ', '.*')
- rx = QRegExp(val, Qt.CaseInsensitive)
- self.setFilterRegExp(rx)
+ rx = QRegularExpression(val, QRegularExpression.CaseInsensitiveOption)
+ qtutils.ensure_valid(rx)
+ self.setFilterRegularExpression(rx)
self.invalidate()
sortcol = 0
self.sort(sortcol)
diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py
index 36e334955..925f95bbb 100644
--- a/qutebrowser/completion/models/miscmodels.py
+++ b/qutebrowser/completion/models/miscmodels.py
@@ -20,7 +20,7 @@
"""Functions that return miscellaneous completion models."""
import datetime
-import typing
+from typing import List, Sequence, Tuple
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, utils
@@ -39,11 +39,11 @@ def command(*, info):
def helptopic(*, info):
"""A CompletionModel filled with help topics."""
- model = completionmodel.CompletionModel()
+ model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
cmdlist = util.get_cmd_completions(info, include_aliases=False,
include_hidden=True, prefix=':')
- settings = ((opt.name, opt.description)
+ settings = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Commands", cmdlist))
@@ -53,7 +53,7 @@ def helptopic(*, info):
def quickmark(*, info=None):
"""A CompletionModel filled with all quickmarks."""
- def delete(data: typing.Sequence[str]) -> None:
+ def delete(data: Sequence[str]) -> None:
"""Delete a quickmark from the completion menu."""
name = data[0]
quickmark_manager = objreg.get('quickmark-manager')
@@ -71,7 +71,7 @@ def quickmark(*, info=None):
def bookmark(*, info=None):
"""A CompletionModel filled with all bookmarks."""
- def delete(data: typing.Sequence[str]) -> None:
+ def delete(data: Sequence[str]) -> None:
"""Delete a bookmark from the completion menu."""
urlstr = data[0]
log.completion.debug('Deleting bookmark {}'.format(urlstr))
@@ -121,7 +121,7 @@ def _buffer(*, win_id_filter=lambda _win_id: True, add_win_id=True):
tabs_are_windows = config.val.tabs.tabs_are_windows
# list storing all single-tabbed windows when tabs_are_windows
- windows = [] # type: typing.List[typing.Tuple[str, str, str]]
+ windows: List[Tuple[str, str, str]] = []
for win_id in objreg.window_registry:
if not win_id_filter(win_id):
@@ -131,7 +131,7 @@ def _buffer(*, win_id_filter=lambda _win_id: True, add_win_id=True):
window=win_id)
if tabbed_browser.is_shutting_down:
continue
- tabs = [] # type: typing.List[typing.Tuple[str, str, str]]
+ tabs: List[Tuple[str, str, str]] = []
for idx in range(tabbed_browser.widget.count()):
tab = tabbed_browser.widget.widget(idx)
tab_str = ("{}/{}".format(win_id, idx + 1) if add_win_id
diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py
index ff83a598a..ba0857d4c 100644
--- a/qutebrowser/completion/models/urlmodel.py
+++ b/qutebrowser/completion/models/urlmodel.py
@@ -19,10 +19,9 @@
"""Function to return the url completion model for the `open` command."""
-import typing
+from typing import Dict, Sequence
-if typing.TYPE_CHECKING:
- from PyQt5.QtCore import QAbstractItemModel
+from PyQt5.QtCore import QAbstractItemModel
from qutebrowser.completion.models import (completionmodel, listcategory,
histcategory)
@@ -41,14 +40,14 @@ def _delete_history(data):
history.web_history.delete_url(urlstr)
-def _delete_bookmark(data: typing.Sequence[str]) -> None:
+def _delete_bookmark(data: Sequence[str]) -> None:
urlstr = data[_URLCOL]
log.completion.debug('Deleting bookmark {}'.format(urlstr))
bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.delete(urlstr)
-def _delete_quickmark(data: typing.Sequence[str]) -> None:
+def _delete_quickmark(data: Sequence[str]) -> None:
name = data[_TEXTCOL]
quickmark_manager = objreg.get('quickmark-manager')
log.completion.debug('Deleting quickmark {}'.format(name))
@@ -77,7 +76,7 @@ def url(*, info):
if k != 'DEFAULT']
# pylint: enable=bad-config-option
categories = config.val.completion.open_categories
- models = {} # type: typing.Dict[str, QAbstractItemModel]
+ models: Dict[str, QAbstractItemModel] = {}
if searchengines and 'searchengines' in categories:
models['searchengines'] = listcategory.ListCategory(
diff --git a/qutebrowser/completion/models/util.py b/qutebrowser/completion/models/util.py
index a0dda334a..7f4f6d19d 100644
--- a/qutebrowser/completion/models/util.py
+++ b/qutebrowser/completion/models/util.py
@@ -19,13 +19,13 @@
"""Utility functions for completion models."""
-import typing
+from typing import Callable, Sequence
from qutebrowser.utils import usertypes
from qutebrowser.misc import objects
-DeleteFuncType = typing.Callable[[typing.Sequence[str]], None]
+DeleteFuncType = Callable[[Sequence[str]], None]
def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py
index b34711fdd..f9b0a583b 100644
--- a/qutebrowser/components/adblock.py
+++ b/qutebrowser/components/adblock.py
@@ -24,8 +24,8 @@ import functools
import posixpath
import zipfile
import logging
-import typing
import pathlib
+from typing import cast, IO, List, Set
from PyQt5.QtCore import QUrl
@@ -34,7 +34,7 @@ from qutebrowser.api import (cmdutils, hook, config, message, downloads,
logger = logging.getLogger('network')
-_host_blocker = typing.cast('HostBlocker', None)
+_host_blocker = cast('HostBlocker', None)
def _guess_zip_filename(zf: zipfile.ZipFile) -> str:
@@ -49,7 +49,7 @@ def _guess_zip_filename(zf: zipfile.ZipFile) -> str:
raise FileNotFoundError("No hosts file found in zip")
-def get_fileobj(byte_io: typing.IO[bytes]) -> typing.IO[bytes]:
+def get_fileobj(byte_io: IO[bytes]) -> IO[bytes]:
"""Get a usable file object to read the hosts file from."""
byte_io.seek(0) # rewind downloaded file
if zipfile.is_zipfile(byte_io):
@@ -75,7 +75,7 @@ class _FakeDownload(downloads.TempDownload):
"""A download stub to use on_download_finished with local files."""
def __init__(self, # pylint: disable=super-init-not-called
- fileobj: typing.IO[bytes]) -> None:
+ fileobj: IO[bytes]) -> None:
self.fileobj = fileobj
self.successful = True
@@ -97,9 +97,9 @@ class HostBlocker:
def __init__(self, *, data_dir: pathlib.Path, config_dir: pathlib.Path,
has_basedir: bool = False) -> None:
self._has_basedir = has_basedir
- self._blocked_hosts = set() # type: typing.Set[str]
- self._config_blocked_hosts = set() # type: typing.Set[str]
- self._in_progress = [] # type: typing.List[downloads.TempDownload]
+ self._blocked_hosts: Set[str] = set()
+ self._config_blocked_hosts: Set[str] = set()
+ self._in_progress: List[downloads.TempDownload] = []
self._done_count = 0
self._local_hosts_file = str(data_dir / 'blocked-hosts')
@@ -132,7 +132,7 @@ class HostBlocker:
.format(info.request_url.host()))
info.block()
- def _read_hosts_line(self, raw_line: bytes) -> typing.Set[str]:
+ def _read_hosts_line(self, raw_line: bytes) -> Set[str]:
"""Read hosts from the given line.
Args:
@@ -170,7 +170,7 @@ class HostBlocker:
return filtered_hosts
- def _read_hosts_file(self, filename: str, target: typing.Set[str]) -> bool:
+ def _read_hosts_file(self, filename: str, target: Set[str]) -> bool:
"""Read hosts from the given filename.
Args:
@@ -246,7 +246,7 @@ class HostBlocker:
self._in_progress.append(download)
self._on_download_finished(download)
- def _merge_file(self, byte_io: typing.IO[bytes]) -> None:
+ def _merge_file(self, byte_io: IO[bytes]) -> None:
"""Read and merge host files.
Args:
diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py
index ff9a21070..f553fce3b 100644
--- a/qutebrowser/components/misccommands.py
+++ b/qutebrowser/components/misccommands.py
@@ -23,7 +23,7 @@ import os
import signal
import functools
import logging
-import typing
+from typing import Optional
try:
import hunter
@@ -41,7 +41,7 @@ from qutebrowser.completion.models import miscmodels
@cmdutils.register(name='reload')
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
-def reloadpage(tab: typing.Optional[apitypes.Tab],
+def reloadpage(tab: Optional[apitypes.Tab],
force: bool = False) -> None:
"""Reload the current/[count]th tab.
@@ -55,7 +55,7 @@ def reloadpage(tab: typing.Optional[apitypes.Tab],
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
-def stop(tab: typing.Optional[apitypes.Tab]) -> None:
+def stop(tab: Optional[apitypes.Tab]) -> None:
"""Stop loading in the current/[count]th tab.
Args:
@@ -97,7 +97,7 @@ def _print_pdf(tab: apitypes.Tab, filename: str) -> None:
@cmdutils.register(name='print')
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
@cmdutils.argument('pdf', flag='f', metavar='file')
-def printpage(tab: typing.Optional[apitypes.Tab],
+def printpage(tab: Optional[apitypes.Tab],
preview: bool = False, *,
pdf: str = None) -> None:
"""Print the current/[count]th tab.
@@ -163,7 +163,7 @@ def insert_text(tab: apitypes.Tab, text: str) -> None:
Args:
text: The text to insert.
"""
- def _insert_text_cb(elem: typing.Optional[apitypes.WebElement]) -> None:
+ def _insert_text_cb(elem: Optional[apitypes.WebElement]) -> None:
if elem is None:
message.error("No element focused!")
return
@@ -195,7 +195,7 @@ def click_element(tab: apitypes.Tab, filter_: str, value: str, *,
target: How to open the clicked element (normal/tab/tab-bg/window).
force_event: Force generating a fake click event.
"""
- def single_cb(elem: typing.Optional[apitypes.WebElement]) -> None:
+ def single_cb(elem: Optional[apitypes.WebElement]) -> None:
"""Click a single element."""
if elem is None:
message.error("No element found with id {}!".format(value))
@@ -236,7 +236,7 @@ def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -> None:
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
-def tab_mute(tab: typing.Optional[apitypes.Tab]) -> None:
+def tab_mute(tab: Optional[apitypes.Tab]) -> None:
"""Mute/Unmute the current/[count]th tab.
Args:
@@ -343,10 +343,3 @@ def devtools_focus(tab: apitypes.Tab) -> None:
tab.data.splitter.cycle_focus()
except apitypes.InspectorError as e:
raise cmdutils.CommandError(e)
-
-
-@cmdutils.register(deprecated='Use :devtools instead')
-@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
-def inspector(tab: apitypes.Tab) -> None:
- """Toggle the web inspector."""
- devtools(tab)
diff --git a/qutebrowser/components/readlinecommands.py b/qutebrowser/components/readlinecommands.py
index 076bb9055..ea8f12edf 100644
--- a/qutebrowser/components/readlinecommands.py
+++ b/qutebrowser/components/readlinecommands.py
@@ -19,7 +19,7 @@
"""Bridge to provide readline-like shortcuts for QLineEdits."""
-import typing
+from typing import Iterable, Optional, MutableMapping
from PyQt5.QtWidgets import QApplication, QLineEdit
@@ -35,9 +35,9 @@ class _ReadlineBridge:
"""
def __init__(self) -> None:
- self._deleted = {} # type: typing.MutableMapping[QLineEdit, str]
+ self._deleted: MutableMapping[QLineEdit, str] = {}
- def _widget(self) -> typing.Optional[QLineEdit]:
+ def _widget(self) -> Optional[QLineEdit]:
"""Get the currently active QLineEdit."""
w = QApplication.instance().focusWidget()
if isinstance(w, QLineEdit):
@@ -86,7 +86,7 @@ class _ReadlineBridge:
def kill_line(self) -> None:
self._dispatch('end', mark=True, delete=True)
- def _rubout(self, delim: typing.Iterable[str]) -> None:
+ def _rubout(self, delim: Iterable[str]) -> None:
"""Delete backwards using the characters in delim as boundaries."""
widget = self._widget()
if widget is None:
diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py
index 007b44404..d8e8d612e 100644
--- a/qutebrowser/config/config.py
+++ b/qutebrowser/config/config.py
@@ -22,8 +22,8 @@
import copy
import contextlib
import functools
-import typing
-from typing import Any
+from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping,
+ MutableMapping, MutableSequence, Optional, Tuple, cast)
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
@@ -32,16 +32,15 @@ from qutebrowser.utils import utils, log, urlmatch
from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils
-if typing.TYPE_CHECKING:
- from typing import Tuple, MutableMapping
+if TYPE_CHECKING:
from qutebrowser.config import configcache, configfiles
from qutebrowser.misc import savemanager
# An easy way to access the config from other code via config.val.foo
-val = typing.cast('ConfigContainer', None)
-instance = typing.cast('Config', None)
-key_instance = typing.cast('KeyConfig', None)
-cache = typing.cast('configcache.ConfigCache', None)
+val = cast('ConfigContainer', None)
+instance = cast('Config', None)
+key_instance = cast('KeyConfig', None)
+cache = cast('configcache.ConfigCache', None)
# Keeping track of all change filters to validate them later.
change_filters = []
@@ -84,7 +83,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
not configdata.is_valid_prefix(self._option)):
raise configexc.NoOptionError(self._option)
- def check_match(self, option: typing.Optional[str]) -> bool:
+ def check_match(self, option: Optional[str]) -> bool:
"""Check if the given option matches the filter."""
if option is None:
# Called directly, not from a config change event.
@@ -97,7 +96,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
else:
return False
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
"""Filter calls to the decorated function.
Gets called when a function should be decorated.
@@ -115,7 +114,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
"""
if self._function:
@functools.wraps(func)
- def func_wrapper(option: str = None) -> typing.Any:
+ def func_wrapper(option: str = None) -> Any:
"""Call the underlying function."""
if self.check_match(option):
return func()
@@ -123,8 +122,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
return func_wrapper
else:
@functools.wraps(func)
- def meth_wrapper(wrapper_self: typing.Any,
- option: str = None) -> typing.Any:
+ def meth_wrapper(wrapper_self: Any, option: str = None) -> Any:
"""Call the underlying function."""
if self.check_match(option):
return func(wrapper_self)
@@ -142,7 +140,7 @@ class KeyConfig:
_config: The Config object to be used.
"""
- _ReverseBindings = typing.Dict[str, typing.MutableSequence[str]]
+ _ReverseBindings = Dict[str, MutableSequence[str]]
def __init__(self, config: 'Config') -> None:
self._config = config
@@ -154,10 +152,7 @@ class KeyConfig:
if mode not in configdata.DATA['bindings.default'].default:
raise configexc.KeybindingError("Invalid mode {}!".format(mode))
- def get_bindings_for(
- self,
- mode: str
- ) -> typing.Dict[keyutils.KeySequence, str]:
+ def get_bindings_for(self, mode: str) -> Dict[keyutils.KeySequence, str]:
"""Get the combined bindings for the given mode."""
bindings = dict(val.bindings.default[mode])
for key, binding in val.bindings.commands[mode].items():
@@ -169,7 +164,7 @@ class KeyConfig:
def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings':
"""Get a dict of commands to a list of bindings for the mode."""
- cmd_to_keys = {} # type: KeyConfig._ReverseBindings
+ cmd_to_keys: KeyConfig._ReverseBindings = {}
bindings = self.get_bindings_for(mode)
for seq, full_cmd in sorted(bindings.items()):
for cmd in full_cmd.split(';;'):
@@ -185,7 +180,7 @@ class KeyConfig:
def get_command(self,
key: keyutils.KeySequence,
mode: str,
- default: bool = False) -> typing.Optional[str]:
+ default: bool = False) -> Optional[str]:
"""Get the command for a given key (or None)."""
self._validate(key, mode)
if default:
@@ -278,19 +273,20 @@ class Config(QObject):
yaml_config: 'configfiles.YamlConfig',
parent: QObject = None) -> None:
super().__init__(parent)
- self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]]
+ self._mutables: MutableMapping[str, Tuple[Any, Any]] = {}
self._yaml = yaml_config
self._init_values()
self.yaml_loaded = False
self.config_py_loaded = False
+ self.warn_autoconfig = True
def _init_values(self) -> None:
"""Populate the self._values dict."""
- self._values = {} # type: typing.Mapping
+ self._values: Mapping = {}
for name, opt in configdata.DATA.items():
self._values[name] = configutils.Values(opt)
- def __iter__(self) -> typing.Iterator[configutils.Values]:
+ def __iter__(self) -> Iterator[configutils.Values]:
"""Iterate over configutils.Values items."""
yield from self._values.values()
@@ -391,7 +387,7 @@ class Config(QObject):
def get_obj_for_pattern(
self, name: str, *,
- pattern: typing.Optional[urlmatch.UrlPattern]
+ pattern: Optional[urlmatch.UrlPattern]
) -> Any:
"""Get the given setting as object (for YAML/config.py).
@@ -525,7 +521,7 @@ class Config(QObject):
Return:
The changed config part as string.
"""
- lines = [] # type: typing.List[str]
+ lines: List[str] = []
for values in sorted(self, key=lambda v: v.opt.name):
lines += values.dump()
@@ -564,7 +560,7 @@ class ConfigContainer:
pattern=self._pattern)
@contextlib.contextmanager
- def _handle_error(self, action: str, name: str) -> typing.Iterator[None]:
+ def _handle_error(self, action: str, name: str) -> Iterator[None]:
try:
yield
except configexc.Error as e:
diff --git a/qutebrowser/config/configcache.py b/qutebrowser/config/configcache.py
index 2bc45f0f8..a07a22ee5 100644
--- a/qutebrowser/config/configcache.py
+++ b/qutebrowser/config/configcache.py
@@ -20,7 +20,7 @@
"""Implementation of a basic config cache."""
-import typing
+from typing import Any, Dict
from qutebrowser.config import config
@@ -38,14 +38,14 @@ class ConfigCache:
"""
def __init__(self) -> None:
- self._cache = {} # type: typing.Dict[str, typing.Any]
+ self._cache: Dict[str, Any] = {}
config.instance.changed.connect(self._on_config_changed)
def _on_config_changed(self, attr: str) -> None:
if attr in self._cache:
del self._cache[attr]
- def __getitem__(self, attr: str) -> typing.Any:
+ def __getitem__(self, attr: str) -> Any:
try:
return self._cache[attr]
except KeyError:
diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py
index 20702be10..0f57b0a03 100644
--- a/qutebrowser/config/configcommands.py
+++ b/qutebrowser/config/configcommands.py
@@ -19,9 +19,9 @@
"""Commands related to the configuration."""
-import typing
import os.path
import contextlib
+from typing import TYPE_CHECKING, Iterator, List, Optional
from PyQt5.QtCore import QUrl
@@ -32,7 +32,7 @@ from qutebrowser.config import configtypes, configexc, configfiles, configdata
from qutebrowser.misc import editor
from qutebrowser.keyinput import keyutils
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.config.config import Config, KeyConfig
@@ -47,17 +47,14 @@ class ConfigCommands:
self._keyconfig = keyconfig
@contextlib.contextmanager
- def _handle_config_error(self) -> typing.Iterator[None]:
+ def _handle_config_error(self) -> Iterator[None]:
"""Catch errors in set_command and raise CommandError."""
try:
yield
except configexc.Error as e:
raise cmdutils.CommandError(str(e))
- def _parse_pattern(
- self,
- pattern: typing.Optional[str]
- ) -> typing.Optional[urlmatch.UrlPattern]:
+ def _parse_pattern(self, pattern: Optional[str]) -> Optional[urlmatch.UrlPattern]:
"""Parse a pattern string argument to a pattern."""
if pattern is None:
return None
@@ -75,8 +72,7 @@ class ConfigCommands:
except keyutils.KeyParseError as e:
raise cmdutils.CommandError(str(e))
- def _print_value(self, option: str,
- pattern: typing.Optional[urlmatch.UrlPattern]) -> None:
+ def _print_value(self, option: str, pattern: Optional[urlmatch.UrlPattern]) -> None:
"""Print the value of the given option."""
with self._handle_config_error():
value = self._config.get_str(option, pattern=pattern)
@@ -263,17 +259,9 @@ class ConfigCommands:
@cmdutils.register(instance='config-commands')
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
- def config_diff(self, win_id: int, old: bool = False) -> None:
- """Show all customized options.
-
- Args:
- old: Show difference for the pre-v1.0 files
- (qutebrowser.conf/keys.conf).
- """
+ def config_diff(self, win_id: int) -> None:
+ """Show all customized options."""
url = QUrl('qute://configdiff')
- if old:
- url.setPath('/old')
-
tabbed_browser = objreg.get('tabbed-browser',
scope='window', window=win_id)
tabbed_browser.load_url(url, newtab=False)
@@ -468,7 +456,7 @@ class ConfigCommands:
raise cmdutils.CommandError("{} already exists - use --force to "
"overwrite!".format(filename))
- options = [] # type: typing.List
+ options: List = []
if defaults:
options = [(None, opt, opt.default)
for _name, opt in sorted(configdata.DATA.items())]
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
index 290d897bd..065527bb9 100644
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -24,8 +24,8 @@ Module attributes:
DATA: A dict of Option objects after init() has been called.
"""
-import typing
-from typing import Optional
+from typing import (Any, Dict, Iterable, List, Mapping, MutableMapping, Optional,
+ Sequence, Tuple, Union, cast)
import functools
import attr
@@ -33,10 +33,10 @@ from qutebrowser.config import configtypes
from qutebrowser.utils import usertypes, qtutils, utils
from qutebrowser.misc import debugcachestats
-DATA = typing.cast(typing.Mapping[str, 'Option'], None)
-MIGRATIONS = typing.cast('Migrations', None)
+DATA = cast(Mapping[str, 'Option'], None)
+MIGRATIONS = cast('Migrations', None)
-_BackendDict = typing.Mapping[str, typing.Union[str, bool]]
+_BackendDict = Mapping[str, Union[str, bool]]
@attr.s
@@ -47,15 +47,15 @@ class Option:
Note that this is just an option which exists, with no value associated.
"""
- name = attr.ib() # type: str
- typ = attr.ib() # type: configtypes.BaseType
- default = attr.ib() # type: typing.Any
- backends = attr.ib() # type: typing.Iterable[usertypes.Backend]
- raw_backends = attr.ib() # type: Optional[typing.Mapping[str, bool]]
- description = attr.ib() # type: str
- supports_pattern = attr.ib(default=False) # type: bool
- restart = attr.ib(default=False) # type: bool
- no_autoconfig = attr.ib(default=False) # type: bool
+ name: str = attr.ib()
+ typ: configtypes.BaseType = attr.ib()
+ default: Any = attr.ib()
+ backends: Iterable[usertypes.Backend] = attr.ib()
+ raw_backends: Optional[Mapping[str, bool]] = attr.ib()
+ description: str = attr.ib()
+ supports_pattern: bool = attr.ib(default=False)
+ restart: bool = attr.ib(default=False)
+ no_autoconfig: bool = attr.ib(default=False)
@attr.s
@@ -68,13 +68,11 @@ class Migrations:
deleted: A list of option names which have been removed.
"""
- renamed = attr.ib(
- default=attr.Factory(dict)) # type: typing.Dict[str, str]
- deleted = attr.ib(
- default=attr.Factory(list)) # type: typing.List[str]
+ renamed: Dict[str, str] = attr.ib(default=attr.Factory(dict))
+ deleted: List[str] = attr.ib(default=attr.Factory(list))
-def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None:
+def _raise_invalid_node(name: str, what: str, node: Any) -> None:
"""Raise an exception for an invalid configdata YAML node.
Args:
@@ -88,14 +86,14 @@ def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None:
def _parse_yaml_type(
name: str,
- node: typing.Union[str, typing.Mapping[str, typing.Any]],
+ node: Union[str, Mapping[str, Any]],
) -> configtypes.BaseType:
if isinstance(node, str):
# e.g:
# > type: Bool
# -> create the type object without any arguments
type_name = node
- kwargs = {} # type: typing.MutableMapping[str, typing.Any]
+ kwargs: MutableMapping[str, Any] = {}
elif isinstance(node, dict):
# e.g:
# > type:
@@ -136,14 +134,14 @@ def _parse_yaml_type(
def _parse_yaml_backends_dict(
name: str,
node: _BackendDict,
-) -> typing.Sequence[usertypes.Backend]:
+) -> Sequence[usertypes.Backend]:
"""Parse a dict definition for backends.
Example:
backends:
QtWebKit: true
- QtWebEngine: Qt 5.9
+ QtWebEngine: Qt 5.15
"""
str_to_backend = {
'QtWebKit': usertypes.Backend.QtWebKit,
@@ -160,14 +158,9 @@ def _parse_yaml_backends_dict(
conditionals = {
True: True,
False: False,
- 'Qt 5.8': qtutils.version_check('5.8'),
- 'Qt 5.9': qtutils.version_check('5.9'),
- 'Qt 5.9.2': qtutils.version_check('5.9.2'),
- 'Qt 5.10': qtutils.version_check('5.10'),
- 'Qt 5.11': qtutils.version_check('5.11'),
- 'Qt 5.12': qtutils.version_check('5.12'),
'Qt 5.13': qtutils.version_check('5.13'),
'Qt 5.14': qtutils.version_check('5.14'),
+ 'Qt 5.15': qtutils.version_check('5.15'),
}
for key in sorted(node.keys()):
if conditionals[node[key]]:
@@ -178,8 +171,8 @@ def _parse_yaml_backends_dict(
def _parse_yaml_backends(
name: str,
- node: typing.Union[None, str, _BackendDict],
-) -> typing.Sequence[usertypes.Backend]:
+ node: Union[None, str, _BackendDict],
+) -> Sequence[usertypes.Backend]:
"""Parse a backend node in the yaml.
It can have one of those four forms:
@@ -188,7 +181,7 @@ def _parse_yaml_backends(
- backend: QtWebEngine -> setting only available with QtWebEngine
- backend:
QtWebKit: true
- QtWebEngine: Qt 5.9
+ QtWebEngine: Qt 5.15
-> setting available based on the given conditionals.
Return:
@@ -208,7 +201,7 @@ def _parse_yaml_backends(
def _read_yaml(
yaml_data: str,
-) -> typing.Tuple[typing.Mapping[str, Option], Migrations]:
+) -> Tuple[Mapping[str, Option], Migrations]:
"""Read config data from a YAML file.
Args:
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 85ddaf6df..645342767 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -293,16 +293,11 @@ auto_save.session:
content.autoplay:
default: true
type: Bool
- backend:
- QtWebEngine: Qt 5.10
- QtWebKit: false
+ backend: QtWebEngine
supports_pattern: true
desc: >-
Automatically start playing `<video>` elements.
- Note: On Qt < 5.11, this option needs a restart and does not support URL
- patterns.
-
content.cache.size:
default: null
type:
@@ -359,9 +354,6 @@ content.cache.appcache:
content.cookies.accept:
default: all
- backend:
- QtWebKit: true
- QtWebEngine: Qt 5.11
supports_pattern: true
type:
name: String
@@ -390,10 +382,7 @@ content.cookies.accept:
content.cookies.store:
default: true
type: Bool
- desc: >-
- Store cookies.
-
- Note this option needs a restart with QtWebEngine on Qt < 5.9.
+ desc: Store cookies.
content.default_encoding:
type: String
@@ -417,9 +406,7 @@ content.unknown_url_scheme_policy:
- allow-all: "Allows all navigation requests to URLs with unknown
schemes."
default: allow-from-user-interaction
- backend:
- QtWebEngine: Qt 5.11
- QtWebKit: false
+ backend: QtWebEngine
supports_pattern: true
desc: >-
How navigation requests to URLs with unknown schemes are handled.
@@ -448,11 +435,7 @@ content.desktop_capture:
type: BoolAsk
default: ask
supports_pattern: true
- desc: >-
- Allow websites to share screen content.
-
- On Qt < 5.10, a dialog box is always displayed, even if this is set to
- "true".
+ desc: Allow websites to share screen content.
content.developer_extras:
deleted: true
@@ -460,9 +443,7 @@ content.developer_extras:
content.dns_prefetch:
default: true
type: Bool
- backend:
- QtWebKit: true
- QtWebEngine: Qt 5.12
+ backend: QtWebEngine
supports_pattern: true
desc: Try to pre-fetch DNS entries to speed up browsing.
@@ -495,9 +476,7 @@ content.mouse_lock:
default: ask
type: BoolAsk
supports_pattern: true
- backend:
- QtWebKit: false
- QtWebEngine: Qt 5.8
+ backend: QtWebEngine
desc: Allow websites to lock your mouse pointer.
content.headers.accept_language:
@@ -575,12 +554,20 @@ content.headers.user_agent:
- qutebrowser_version
completions:
# See https://techblog.willshouse.com/2012/01/03/most-common-user-agents/
- - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
- like Gecko) Chrome/83.0.4103.61 Safari/537.36"
- - Chrome 83 Win10
+ #
+ # To update the following list of user agents, run the script
+ # 'ua_fetch.py'
+ # Vim-protip: Place your cursor below this comment and run
+ # :r!python scripts/dev/ua_fetch.py
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
- Gecko) Chrome/83.0.4103.61 Safari/537.36"
- - Chrome 83 Linux
+ Gecko) Chrome/86.0.4240.75 Safari/537.36"
+ - Chrome 86 Linux
+ - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
+ like Gecko) Chrome/86.0.4240.75 Safari/537.36"
+ - Chrome 86 Win10
+ - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
+ (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36"
+ - Chrome 86 macOS
supports_pattern: true
desc: |
User agent to send.
@@ -807,9 +794,7 @@ content.persistent_storage:
default: ask
type: BoolAsk
supports_pattern: true
- backend:
- QtWebKit: false
- QtWebEngine: Qt 5.11
+ backend: QtWebEngine
desc: Allow websites to request persistent storage quota via
`navigator.webkitPersistentStorage.requestQuota`.
@@ -822,9 +807,7 @@ content.plugins:
content.print_element_backgrounds:
type: Bool
default: true
- backend:
- QtWebKit: true
- QtWebEngine: Qt 5.8
+ backend: QtWebEngine
supports_pattern: true
desc: >-
Draw the background color and images also when the page is printed.
@@ -857,9 +840,7 @@ content.register_protocol_handler:
default: ask
type: BoolAsk
supports_pattern: true
- backend:
- QtWebKit: false
- QtWebEngine: Qt 5.11
+ backend: QtWebEngine
desc: Allow websites to register protocol handlers via
`navigator.registerProtocolHandler`.
@@ -899,15 +880,11 @@ content.webrtc_ip_handling_policy:
servers unless the proxy server supports UDP. This doesn't expose
any local addresses either.
default: all-interfaces
- backend:
- QtWebKit: false
- QtWebEngine: Qt 5.9.2
+ backend: QtWebEngine
restart: true
desc: >-
Which interfaces to expose via WebRTC.
- On Qt 5.10, this option doesn't work because of a Qt bug.
-
content.xss_auditing:
type: Bool
default: false
@@ -1529,7 +1506,7 @@ scrolling.bar:
- never: Never show the scrollbar.
- when-searching: Show the scrollbar when searching for text in the
webpage. With the QtWebKit backend, this is equal to `never`.
- - overlay: Show an overlay scrollbar. With Qt < 5.11 or on macOS, this is
+ - overlay: Show an overlay scrollbar. On macOS, this is
unavailable and equal to `when-searching`; with the QtWebKit
backend, this is equal to `never`. Enabling/disabling overlay
scrollbars requires a restart.
@@ -1603,9 +1580,7 @@ spellcheck.languages:
You can check for available languages and install dictionaries using
scripts/dictcli.py. Run the script with -h/--help for instructions.
- backend:
- QtWebKit: false
- QtWebEngine: Qt 5.8
+ backend: QtWebEngine
## statusbar
@@ -1827,14 +1802,14 @@ tabs.title.format:
* `{perc}`: Percentage as a string like `[10%]`.
* `{perc_raw}`: Raw percentage, e.g. `10`.
* `{current_title}`: Title of the current web page.
- * `{title_sep}`: The string ` - ` if a title is set, empty otherwise.
+ * `{title_sep}`: The string `" - "` if a title is set, empty otherwise.
* `{index}`: Index of this tab.
* `{aligned_index}`: Index of this tab padded with spaces to have the same
width.
* `{id}`: Internal tab ID of this tab.
* `{scroll_pos}`: Page scroll position.
* `{host}`: Host of the current web page.
- * `{backend}`: Either ''webkit'' or ''webengine''
+ * `{backend}`: Either `webkit` or `webengine`
* `{private}`: Indicates when private mode is enabled.
* `{current_url}`: URL of the current web page.
* `{protocol}`: Protocol (http/https/...) of the current web page.
@@ -2037,6 +2012,7 @@ url.searchengines:
expands to `slash%2Fand%26amp`).
* `{unquoted}` quotes nothing (for `slash/and&amp` this placeholder
expands to `slash/and&amp`).
+ * `{0}` means the same as `{}`, but can be used multiple times.
The search engine named `DEFAULT` is used when `url.auto_search` is turned
on and something else than a URL was entered to be opened. Other search
@@ -2098,6 +2074,19 @@ window.title_format:
Format to use for the window title. The same placeholders like for
`tabs.title.format` are defined.
+window.transparent:
+ type: Bool
+ default: false
+ desc: |
+ Set the main window background to transparent.
+
+ This allows having a transparent tab- or statusbar (might require a compositor such
+ as picom). However, it breaks some functionality such as dmenu embedding via its
+ `-w` option. On some systems, it was additionally reported that main window
+ transparency negatively affects performance.
+
+ Note this setting only affects windows opened after setting it.
+
## zoom
zoom.default:
@@ -2677,6 +2666,7 @@ colors.webpage.prefers_color_scheme_dark:
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
+ restart: true
## dark mode
@@ -2702,19 +2692,20 @@ colors.webpage.darkmode.enabled:
- "With selective inversion of everything": Combines the two variants
above.
restart: true
- backend:
- QtWebEngine: Qt 5.14
- QtWebKit: false
+ backend: QtWebEngine
colors.webpage.darkmode.algorithm:
default: lightness-cielab
- desc: "Which algorithm to use for modifying how colors are rendered with
- darkmode."
+ desc: >-
+ Which algorithm to use for modifying how colors are rendered with darkmode.
+
+ The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated
+ like `lightness-hsl` with older QtWebEngine versions.
type:
name: String
valid_values:
- lightness-cielab: Modify colors by converting them to CIELAB color
- space and inverting the L value.
+ space and inverting the L value. Not available with Qt < 5.14.
- lightness-hsl: Modify colors by converting them to the HSL color space
and inverting the lightness (i.e. the "L" in HSL).
- brightness-rgb: Modify colors by subtracting each of r, g, and b from
@@ -2723,9 +2714,7 @@ colors.webpage.darkmode.algorithm:
# kInvertBrightness without gamma correction, and only available for
# Chromium's automated tests
restart: true
- backend:
- QtWebEngine: Qt 5.14
- QtWebKit: false
+ backend: QtWebEngine
colors.webpage.darkmode.contrast:
default: 0.0
@@ -2739,29 +2728,26 @@ colors.webpage.darkmode.contrast:
This only has an effect when `colors.webpage.darkmode.algorithm` is set to
`lightness-hsl` or `brightness-rgb`.
restart: true
- backend:
- QtWebEngine: Qt 5.14
- QtWebKit: false
+ backend: QtWebEngine
colors.webpage.darkmode.policy.images:
- default: never
+ default: smart
type:
name: String
valid_values:
- always: Apply dark mode filter to all images.
- never: Never apply dark mode filter to any images.
- - smart: Apply dark mode based on image content.
+ - smart: "Apply dark mode based on image content. Not available with Qt
+ 5.15.0."
desc: >-
Which images to apply dark mode to.
- WARNING: On Qt 5.15.0, this setting can cause frequent renderer process
+ With QtWebEngine 5.15.0, this setting can cause frequent renderer process
crashes due to a
https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug
in Qt].
restart: true
- backend:
- QtWebEngine: Qt 5.14
- QtWebKit: false
+ backend: QtWebEngine
colors.webpage.darkmode.policy.page:
default: smart
@@ -2822,9 +2808,7 @@ colors.webpage.darkmode.grayscale.all:
This only has an effect when `colors.webpage.darkmode.algorithm` is set to
`lightness-hsl` or `brightness-rgb`.
restart: true
- backend:
- QtWebEngine: Qt 5.14
- QtWebKit: false
+ backend: QtWebEngine
colors.webpage.darkmode.grayscale.images:
default: 0.0
diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py
deleted file mode 100644
index 53177134b..000000000
--- a/qutebrowser/config/configdiff.py
+++ /dev/null
@@ -1,761 +0,0 @@
-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2017-2020 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 <http://www.gnu.org/licenses/>.
-
-"""Code to show a diff of the legacy config format."""
-
-import typing
-import difflib
-import os.path
-
-import pygments
-import pygments.lexers
-import pygments.formatters
-
-from qutebrowser.utils import standarddir
-
-
-OLD_CONF = """
-[general]
-ignore-case = smart
-startpage = https://start.duckduckgo.com
-yank-ignored-url-parameters = ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content
-default-open-dispatcher =
-default-page = ${startpage}
-auto-search = naive
-auto-save-config = true
-auto-save-interval = 15000
-editor = gvim -f "{}"
-editor-encoding = utf-8
-private-browsing = false
-developer-extras = false
-print-element-backgrounds = true
-xss-auditing = false
-default-encoding = iso-8859-1
-new-instance-open-target = tab
-new-instance-open-target.window = last-focused
-log-javascript-console = debug
-save-session = false
-session-default-name =
-url-incdec-segments = path,query
-[ui]
-history-session-interval = 30
-zoom-levels = 25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500%
-default-zoom = 100%
-downloads-position = top
-status-position = bottom
-message-timeout = 2000
-message-unfocused = false
-confirm-quit = never
-zoom-text-only = false
-frame-flattening = false
-user-stylesheet =
-hide-scrollbar = true
-smooth-scrolling = false
-remove-finished-downloads = -1
-hide-statusbar = false
-statusbar-padding = 1,1,0,0
-window-title-format = {perc}{title}{title_sep}qutebrowser
-modal-js-dialog = false
-hide-wayland-decoration = false
-keyhint-blacklist =
-keyhint-delay = 500
-prompt-radius = 8
-prompt-filebrowser = true
-[network]
-do-not-track = true
-accept-language = en-US,en
-referer-header = same-domain
-user-agent =
-proxy = system
-proxy-dns-requests = true
-ssl-strict = ask
-dns-prefetch = true
-custom-headers =
-netrc-file =
-[completion]
-show = always
-download-path-suggestion = path
-timestamp-format = %Y-%m-%d
-height = 50%
-cmd-history-max-items = 100
-web-history-max-items = -1
-quick-complete = true
-shrink = false
-scrollbar-width = 12
-scrollbar-padding = 2
-[input]
-timeout = 500
-partial-timeout = 5000
-insert-mode-on-plugins = false
-auto-leave-insert-mode = true
-auto-insert-mode = false
-forward-unbound-keys = auto
-spatial-navigation = false
-links-included-in-focus-chain = true
-rocker-gestures = false
-mouse-zoom-divider = 512
-[tabs]
-background-tabs = false
-select-on-remove = next
-new-tab-position = next
-new-tab-position-explicit = last
-last-close = ignore
-show = always
-show-switching-delay = 800
-wrap = true
-movable = true
-close-mouse-button = middle
-position = top
-show-favicons = true
-favicon-scale = 1.0
-width = 20%
-pinned-width = 43
-indicator-width = 3
-tabs-are-windows = false
-title-format = {index}: {title}
-title-format-pinned = {index}
-title-alignment = left
-mousewheel-tab-switching = true
-padding = 0,0,5,5
-indicator-padding = 2,2,0,4
-[storage]
-download-directory =
-prompt-download-directory = true
-remember-download-directory = true
-maximum-pages-in-cache = 0
-offline-web-application-cache = true
-local-storage = true
-cache-size =
-[content]
-allow-images = true
-allow-javascript = true
-allow-plugins = false
-webgl = true
-hyperlink-auditing = false
-geolocation = ask
-notifications = ask
-media-capture = ask
-javascript-can-open-windows-automatically = false
-javascript-can-close-windows = false
-javascript-can-access-clipboard = false
-ignore-javascript-prompt = false
-ignore-javascript-alert = false
-local-content-can-access-remote-urls = false
-local-content-can-access-file-urls = true
-cookies-accept = no-3rdparty
-cookies-store = true
-host-block-lists = https://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext
-host-blocking-enabled = true
-host-blocking-whitelist = piwik.org
-enable-pdfjs = false
-[hints]
-border = 1px solid #E3BE23
-mode = letter
-chars = asdfghjkl
-min-chars = 1
-scatter = true
-uppercase = false
-dictionary = /usr/share/dict/words
-auto-follow = unique-match
-auto-follow-timeout = 0
-next-regexes = \\bnext\\b,\\bmore\\b,\\bnewer\\b,\\b[>\u2192\u226b]\\b,\\b(>>|\xbb)\\b,\\bcontinue\\b
-prev-regexes = \\bprev(ious)?\\b,\\bback\\b,\\bolder\\b,\\b[<\u2190\u226a]\\b,\\b(<<|\xab)\\b
-find-implementation = python
-hide-unmatched-rapid-hints = true
-[searchengines]
-DEFAULT = https://duckduckgo.com/?q={}
-[aliases]
-[colors]
-completion.fg = white
-completion.bg = #333333
-completion.alternate-bg = #444444
-completion.category.fg = white
-completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050)
-completion.category.border.top = black
-completion.category.border.bottom = ${completion.category.border.top}
-completion.item.selected.fg = black
-completion.item.selected.bg = #e8c000
-completion.item.selected.border.top = #bbbb00
-completion.item.selected.border.bottom = ${completion.item.selected.border.top}
-completion.match.fg = #ff4444
-completion.scrollbar.fg = ${completion.fg}
-completion.scrollbar.bg = ${completion.bg}
-statusbar.fg = white
-statusbar.bg = black
-statusbar.fg.private = ${statusbar.fg}
-statusbar.bg.private = #666666
-statusbar.fg.insert = ${statusbar.fg}
-statusbar.bg.insert = darkgreen
-statusbar.fg.command = ${statusbar.fg}
-statusbar.bg.command = ${statusbar.bg}
-statusbar.fg.command.private = ${statusbar.fg.private}
-statusbar.bg.command.private = ${statusbar.bg.private}
-statusbar.fg.caret = ${statusbar.fg}
-statusbar.bg.caret = purple
-statusbar.fg.caret-selection = ${statusbar.fg}
-statusbar.bg.caret-selection = #a12dff
-statusbar.progress.bg = white
-statusbar.url.fg = ${statusbar.fg}
-statusbar.url.fg.success = white
-statusbar.url.fg.success.https = lime
-statusbar.url.fg.error = orange
-statusbar.url.fg.warn = yellow
-statusbar.url.fg.hover = aqua
-tabs.fg.odd = white
-tabs.bg.odd = grey
-tabs.fg.even = white
-tabs.bg.even = darkgrey
-tabs.fg.selected.odd = white
-tabs.bg.selected.odd = black
-tabs.fg.selected.even = ${tabs.fg.selected.odd}
-tabs.bg.selected.even = ${tabs.bg.selected.odd}
-tabs.bg.bar = #555555
-tabs.indicator.start = #0000aa
-tabs.indicator.stop = #00aa00
-tabs.indicator.error = #ff0000
-tabs.indicator.system = rgb
-hints.fg = black
-hints.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8))
-hints.fg.match = green
-downloads.bg.bar = black
-downloads.fg.start = white
-downloads.bg.start = #0000aa
-downloads.fg.stop = ${downloads.fg.start}
-downloads.bg.stop = #00aa00
-downloads.fg.system = rgb
-downloads.bg.system = rgb
-downloads.fg.error = white
-downloads.bg.error = red
-webpage.bg = white
-keyhint.fg = #FFFFFF
-keyhint.fg.suffix = #FFFF00
-keyhint.bg = rgba(0, 0, 0, 80%)
-messages.fg.error = white
-messages.bg.error = red
-messages.border.error = #bb0000
-messages.fg.warning = white
-messages.bg.warning = darkorange
-messages.border.warning = #d47300
-messages.fg.info = white
-messages.bg.info = black
-messages.border.info = #333333
-prompts.fg = white
-prompts.bg = darkblue
-prompts.selected.bg = #308cc6
-[fonts]
-_monospace = xos4 Terminus, Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal
-completion = 8pt ${_monospace}
-completion.category = bold ${completion}
-tabbar = 8pt ${_monospace}
-statusbar = 8pt ${_monospace}
-downloads = 8pt ${_monospace}
-hints = bold 13px ${_monospace}
-debug-console = 8pt ${_monospace}
-web-family-standard =
-web-family-fixed =
-web-family-serif =
-web-family-sans-serif =
-web-family-cursive =
-web-family-fantasy =
-web-size-minimum = 0
-web-size-minimum-logical = 6
-web-size-default = 16
-web-size-default-fixed = 13
-keyhint = 8pt ${_monospace}
-messages.error = 8pt ${_monospace}
-messages.warning = 8pt ${_monospace}
-messages.info = 8pt ${_monospace}
-prompts = 8pt sans-serif
-"""
-
-OLD_KEYS_CONF = """
-[!normal]
-leave-mode
- <escape>
- <ctrl-[>
-[normal]
-clear-keychain ;; search ;; fullscreen --leave
- <escape>
- <ctrl-[>
-set-cmd-text -s :open
- o
-set-cmd-text :open {url:pretty}
- go
-set-cmd-text -s :open -t
- O
-set-cmd-text :open -t -i {url:pretty}
- gO
-set-cmd-text -s :open -b
- xo
-set-cmd-text :open -b -i {url:pretty}
- xO
-set-cmd-text -s :open -w
- wo
-set-cmd-text :open -w {url:pretty}
- wO
-set-cmd-text /
- /
-set-cmd-text ?
- ?
-set-cmd-text :
- :
-open -t
- ga
- <ctrl-t>
-open -w
- <ctrl-n>
-tab-close
- d
- <ctrl-w>
-tab-close -o
- D
-tab-only
- co
-tab-focus
- T
-tab-move
- gm
-tab-move -
- gl
-tab-move +
- gr
-tab-next
- J
- <ctrl-pgdown>
-tab-prev
- K
- <ctrl-pgup>
-tab-clone
- gC
-reload
- r
- <f5>
-reload -f
- R
- <ctrl-f5>
-back
- H
- <back>
-back -t
- th
-back -w
- wh
-forward
- L
- <forward>
-forward -t
- tl
-forward -w
- wl
-fullscreen
- <f11>
-hint
- f
-hint all tab
- F
-hint all window
- wf
-hint all tab-bg
- ;b
-hint all tab-fg
- ;f
-hint all hover
- ;h
-hint images
- ;i
-hint images tab
- ;I
-hint links fill :open {hint-url}
- ;o
-hint links fill :open -t -i {hint-url}
- ;O
-hint links yank
- ;y
-hint links yank-primary
- ;Y
-hint --rapid links tab-bg
- ;r
-hint --rapid links window
- ;R
-hint links download
- ;d
-hint inputs
- ;t
-scroll left
- h
-scroll down
- j
-scroll up
- k
-scroll right
- l
-undo
- u
- <ctrl-shift-t>
-scroll-perc 0
- gg
-scroll-perc
- G
-search-next
- n
-search-prev
- N
-enter-mode insert
- i
-enter-mode caret
- v
-enter-mode set_mark
- `
-enter-mode jump_mark
- '
-yank
- yy
-yank -s
- yY
-yank title
- yt
-yank title -s
- yT
-yank domain
- yd
-yank domain -s
- yD
-yank pretty-url
- yp
-yank pretty-url -s
- yP
-open -- {clipboard}
- pp
-open -- {primary}
- pP
-open -t -- {clipboard}
- Pp
-open -t -- {primary}
- PP
-open -w -- {clipboard}
- wp
-open -w -- {primary}
- wP
-quickmark-save
- m
-set-cmd-text -s :quickmark-load
- b
-set-cmd-text -s :quickmark-load -t
- B
-set-cmd-text -s :quickmark-load -w
- wb
-bookmark-add
- M
-set-cmd-text -s :bookmark-load
- gb
-set-cmd-text -s :bookmark-load -t
- gB
-set-cmd-text -s :bookmark-load -w
- wB
-save
- sf
-set-cmd-text -s :set
- ss
-set-cmd-text -s :set -t
- sl
-set-cmd-text -s :bind
- sk
-zoom-out
- -
-zoom-in
- +
-zoom
- =
-navigate prev
- [[
-navigate next
- ]]
-navigate prev -t
- {{
-navigate next -t
- }}
-navigate up
- gu
-navigate up -t
- gU
-navigate increment
- <ctrl-a>
-navigate decrement
- <ctrl-x>
-inspector
- wi
-download
- gd
-download-cancel
- ad
-download-clear
- cd
-view-source
- gf
-set-cmd-text -s :buffer
- gt
-tab-focus last
- <ctrl-tab>
- <ctrl-6>
- <ctrl-^>
-enter-mode passthrough
- <ctrl-v>
-quit
- <ctrl-q>
- ZQ
-wq
- ZZ
-scroll-page 0 1
- <ctrl-f>
-scroll-page 0 -1
- <ctrl-b>
-scroll-page 0 0.5
- <ctrl-d>
-scroll-page 0 -0.5
- <ctrl-u>
-tab-focus 1
- <alt-1>
- g0
- g^
-tab-focus 2
- <alt-2>
-tab-focus 3
- <alt-3>
-tab-focus 4
- <alt-4>
-tab-focus 5
- <alt-5>
-tab-focus 6
- <alt-6>
-tab-focus 7
- <alt-7>
-tab-focus 8
- <alt-8>
-tab-focus -1
- <alt-9>
- g$
-home
- <ctrl-h>
-stop
- <ctrl-s>
-print
- <ctrl-alt-p>
-open qute://settings
- Ss
-follow-selected
- <return>
- <ctrl-m>
- <ctrl-j>
- <shift-return>
- <enter>
- <shift-enter>
-follow-selected -t
- <ctrl-return>
- <ctrl-enter>
-repeat-command
- .
-tab-pin
- <ctrl-p>
-record-macro
- q
-run-macro
- @
-[insert]
-open-editor
- <ctrl-e>
-insert-text {primary}
- <shift-ins>
-[hint]
-follow-hint
- <return>
- <ctrl-m>
- <ctrl-j>
- <shift-return>
- <enter>
- <shift-enter>
-hint --rapid links tab-bg
- <ctrl-r>
-hint links
- <ctrl-f>
-hint all tab-bg
- <ctrl-b>
-[passthrough]
-[command]
-command-history-prev
- <ctrl-p>
-command-history-next
- <ctrl-n>
-completion-item-focus prev
- <shift-tab>
- <up>
-completion-item-focus next
- <tab>
- <down>
-completion-item-focus next-category
- <ctrl-tab>
-completion-item-focus prev-category
- <ctrl-shift-tab>
-completion-item-del
- <ctrl-d>
-command-accept
- <return>
- <ctrl-m>
- <ctrl-j>
- <shift-return>
- <enter>
- <shift-enter>
-[prompt]
-prompt-accept
- <return>
- <ctrl-m>
- <ctrl-j>
- <shift-return>
- <enter>
- <shift-enter>
-prompt-accept yes
- y
-prompt-accept no
- n
-prompt-open-download
- <ctrl-x>
-prompt-item-focus prev
- <shift-tab>
- <up>
-prompt-item-focus next
- <tab>
- <down>
-[command,prompt]
-rl-backward-char
- <ctrl-b>
-rl-forward-char
- <ctrl-f>
-rl-backward-word
- <alt-b>
-rl-forward-word
- <alt-f>
-rl-beginning-of-line
- <ctrl-a>
-rl-end-of-line
- <ctrl-e>
-rl-unix-line-discard
- <ctrl-u>
-rl-kill-line
- <ctrl-k>
-rl-kill-word
- <alt-d>
-rl-unix-word-rubout
- <ctrl-w>
-rl-backward-kill-word
- <alt-backspace>
-rl-yank
- <ctrl-y>
-rl-delete-char
- <ctrl-?>
-rl-backward-delete-char
- <ctrl-h>
-[caret]
-toggle-selection
- v
- <space>
-drop-selection
- <ctrl-space>
-enter-mode normal
- c
-move-to-next-line
- j
-move-to-prev-line
- k
-move-to-next-char
- l
-move-to-prev-char
- h
-move-to-end-of-word
- e
-move-to-next-word
- w
-move-to-prev-word
- b
-move-to-start-of-next-block
- ]
-move-to-start-of-prev-block
- [
-move-to-end-of-next-block
- }
-move-to-end-of-prev-block
- {
-move-to-start-of-line
- 0
-move-to-end-of-line
- $
-move-to-start-of-document
- gg
-move-to-end-of-document
- G
-yank selection -s
- Y
-yank selection
- y
- <return>
- <ctrl-m>
- <ctrl-j>
- <shift-return>
- <enter>
- <shift-enter>
-scroll left
- H
-scroll down
- J
-scroll up
- K
-scroll right
- L
-"""
-
-
-def get_diff() -> str:
- """Get a HTML diff for the old config files."""
- old_conf_lines = [] # type: typing.MutableSequence[str]
- old_key_lines = [] # type: typing.MutableSequence[str]
-
- for filename, dest in [('qutebrowser.conf', old_conf_lines),
- ('keys.conf', old_key_lines)]:
- path = os.path.join(standarddir.config(), filename)
-
- with open(path, 'r', encoding='utf-8') as f:
- for line in f:
- if not line.strip() or line.startswith('#'):
- continue
- dest.append(line.rstrip())
-
- conf_delta = difflib.unified_diff(OLD_CONF.lstrip().splitlines(),
- old_conf_lines)
- key_delta = difflib.unified_diff(OLD_KEYS_CONF.lstrip().splitlines(),
- old_key_lines)
-
- conf_diff = '\n'.join(conf_delta)
- key_diff = '\n'.join(key_delta)
-
- # pylint: disable=no-member
- # WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/
- lexer = pygments.lexers.DiffLexer()
- formatter = pygments.formatters.HtmlFormatter(
- full=True, linenos='table',
- title='Diffing pre-1.0 default config with pre-1.0 modified config')
- # pylint: enable=no-member
- return pygments.highlight(conf_diff + key_diff, lexer, formatter)
diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py
index b409bc883..872f777ff 100644
--- a/qutebrowser/config/configexc.py
+++ b/qutebrowser/config/configexc.py
@@ -19,9 +19,9 @@
"""Exceptions related to config parsing."""
-import typing
-import attr
+from typing import Any, Mapping, Optional, Sequence, Union
+import attr
from qutebrowser.utils import usertypes, log
@@ -46,7 +46,7 @@ class BackendError(Error):
def __init__(
self, name: str,
backend: usertypes.Backend,
- raw_backends: typing.Optional[typing.Mapping[str, bool]]
+ raw_backends: Optional[Mapping[str, bool]]
) -> None:
if raw_backends is None or not raw_backends[backend.name]:
msg = ("The {} setting is not available with the {} backend!"
@@ -76,8 +76,7 @@ class ValidationError(Error):
msg: Additional error message.
"""
- def __init__(self, value: typing.Any,
- msg: typing.Union[str, Exception]) -> None:
+ def __init__(self, value: Any, msg: Union[str, Exception]) -> None:
super().__init__("Invalid value '{}' - {}".format(value, msg))
self.option = None
@@ -117,9 +116,9 @@ class ConfigErrorDesc:
traceback: The formatted traceback of the exception.
"""
- text = attr.ib() # type: str
- exception = attr.ib() # type: typing.Union[str, Exception]
- traceback = attr.ib(None) # type: str
+ text: str = attr.ib()
+ exception: Union[str, Exception] = attr.ib()
+ traceback: str = attr.ib(None)
def __str__(self) -> str:
if self.traceback:
@@ -141,7 +140,7 @@ class ConfigFileErrors(Error):
def __init__(self,
basename: str,
- errors: typing.Sequence[ConfigErrorDesc], *,
+ errors: Sequence[ConfigErrorDesc], *,
fatal: bool = False) -> None:
super().__init__("Errors occurred while reading {}:\n{}".format(
basename, '\n'.join(' {}'.format(e) for e in errors)))
diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py
index 9940a64ac..5569174c9 100644
--- a/qutebrowser/config/configfiles.py
+++ b/qutebrowser/config/configfiles.py
@@ -27,8 +27,9 @@ import textwrap
import traceback
import configparser
import contextlib
-import typing
import re
+from typing import (TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Mapping,
+ MutableMapping, Optional, cast)
import yaml
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSettings, qVersion
@@ -39,15 +40,15 @@ from qutebrowser.config import (configexc, config, configdata, configutils,
from qutebrowser.keyinput import keyutils
from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.misc import savemanager
# The StateConfig instance
-state = typing.cast('StateConfig', None)
+state = cast('StateConfig', None)
-_SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]]
+_SettingsType = Dict[str, Dict[str, Any]]
class StateConfig(configparser.ConfigParser):
@@ -77,6 +78,7 @@ class StateConfig(configparser.ConfigParser):
deleted_keys = [
('general', 'fooled'),
('general', 'backend-warning-shown'),
+ ('general', 'old-qt-warning-shown'),
('geometry', 'inspector'),
]
for sect, key in deleted_keys:
@@ -117,7 +119,7 @@ class YamlConfig(QObject):
'autoconfig.yml')
self._dirty = False
- self._values = {} # type: typing.Dict[str, configutils.Values]
+ self._values: Dict[str, configutils.Values] = {}
for name, opt in configdata.DATA.items():
self._values[name] = configutils.Values(opt)
@@ -130,7 +132,7 @@ class YamlConfig(QObject):
"""
save_manager.add_saveable('yaml-config', self._save, self.changed)
- def __iter__(self) -> typing.Iterator[configutils.Values]:
+ def __iter__(self) -> Iterator[configutils.Values]:
"""Iterate over configutils.Values items."""
yield from self._values.values()
@@ -145,7 +147,7 @@ class YamlConfig(QObject):
if not self._dirty:
return
- settings = {} # type: _SettingsType
+ settings: _SettingsType = {}
for name, values in sorted(self._values.items()):
if not values:
continue
@@ -167,10 +169,7 @@ class YamlConfig(QObject):
""".lstrip('\n')))
utils.yaml_dump(data, f)
- def _pop_object(self,
- yaml_data: typing.Any,
- key: str,
- typ: type) -> typing.Any:
+ def _pop_object(self, yaml_data: Any, key: str, typ: type) -> Any:
"""Get a global object from the given data."""
if not isinstance(yaml_data, dict):
desc = configexc.ConfigErrorDesc("While loading data",
@@ -227,19 +226,18 @@ class YamlConfig(QObject):
self._validate_names(settings)
self._build_values(settings)
- def _load_settings_object(self, yaml_data: typing.Any) -> '_SettingsType':
+ def _load_settings_object(self, yaml_data: Any) -> '_SettingsType':
"""Load the settings from the settings: key."""
return self._pop_object(yaml_data, 'settings', dict)
- def _load_legacy_settings_object(self,
- yaml_data: typing.Any) -> '_SettingsType':
+ def _load_legacy_settings_object(self, yaml_data: Any) -> '_SettingsType':
data = self._pop_object(yaml_data, 'global', dict)
settings = {}
for name, value in data.items():
settings[name] = {'global': value}
return settings
- def _build_values(self, settings: typing.Mapping) -> None:
+ def _build_values(self, settings: Mapping) -> None:
"""Build up self._values from the values in the given dict."""
errors = []
for name, yaml_values in settings.items():
@@ -285,7 +283,7 @@ class YamlConfig(QObject):
for e in sorted(unknown)]
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
- def set_obj(self, name: str, value: typing.Any, *,
+ def set_obj(self, name: str, value: Any, *,
pattern: urlmatch.UrlPattern = None) -> None:
"""Set the given setting to the given value."""
self._values[name].add(value, pattern)
@@ -477,8 +475,7 @@ class YamlMigrations(QObject):
self._settings[name][scope] = value
self.changed.emit()
- def _migrate_to_multiple(self, old_name: str,
- new_names: typing.Iterable[str]) -> None:
+ def _migrate_to_multiple(self, old_name: str, new_names: Iterable[str]) -> None:
if old_name not in self._settings:
return
@@ -541,12 +538,12 @@ class ConfigAPI:
def __init__(self, conf: config.Config, keyconfig: config.KeyConfig):
self._config = conf
self._keyconfig = keyconfig
- self.errors = [] # type: typing.List[configexc.ConfigErrorDesc]
+ self.errors: List[configexc.ConfigErrorDesc] = []
self.configdir = pathlib.Path(standarddir.config())
self.datadir = pathlib.Path(standarddir.data())
@contextlib.contextmanager
- def _handle_error(self, action: str, name: str) -> typing.Iterator[None]:
+ def _handle_error(self, action: str, name: str) -> Iterator[None]:
"""Catch config-related exceptions and save them in self.errors."""
try:
yield
@@ -566,28 +563,35 @@ class ConfigAPI:
def finalize(self) -> None:
"""Do work which needs to be done after reading config.py."""
+ if self._config.warn_autoconfig:
+ desc = configexc.ConfigErrorDesc(
+ "autoconfig loading not specified",
+ ("Your config.py should call either `config.load_autoconfig()`"
+ " (to load settings configured via the GUI) or "
+ "`config.load_autoconfig(False)` (to not do so)"))
+ self.errors.append(desc)
self._config.update_mutables()
- def load_autoconfig(self) -> None:
+ def load_autoconfig(self, load_config: bool = True) -> None:
"""Load the autoconfig.yml file which is used for :set/:bind/etc."""
- with self._handle_error('reading', 'autoconfig.yml'):
- read_autoconfig()
+ self._config.warn_autoconfig = False
+ if load_config:
+ with self._handle_error('reading', 'autoconfig.yml'):
+ read_autoconfig()
- def get(self, name: str, pattern: str = None) -> typing.Any:
+ def get(self, name: str, pattern: str = None) -> Any:
"""Get a setting value from the config, optionally with a pattern."""
with self._handle_error('getting', name):
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
return self._config.get_mutable_obj(name, pattern=urlpattern)
- def set(self, name: str, value: typing.Any, pattern: str = None) -> None:
+ def set(self, name: str, value: Any, pattern: str = None) -> None:
"""Set a setting value in the config, optionally with a pattern."""
with self._handle_error('setting', name):
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
self._config.set_obj(name, value, pattern=urlpattern)
- def bind(self, key: str,
- command: typing.Optional[str],
- mode: str = 'normal') -> None:
+ def bind(self, key: str, command: Optional[str], mode: str = 'normal') -> None:
"""Bind a key to a command, with an optional key mode."""
with self._handle_error('binding', key):
seq = keyutils.KeySequence.parse(key)
@@ -606,7 +610,9 @@ class ConfigAPI:
def source(self, filename: str) -> None:
"""Read the given config file from disk."""
if not os.path.isabs(filename):
- filename = str(self.configdir / filename)
+ # We don't use self.configdir here so we get the proper file when starting
+ # with a --config-py argument given.
+ filename = os.path.join(os.path.dirname(standarddir.config_py()), filename)
try:
read_config_py(filename)
@@ -614,7 +620,7 @@ class ConfigAPI:
self.errors += e.errors
@contextlib.contextmanager
- def pattern(self, pattern: str) -> typing.Iterator[config.ConfigContainer]:
+ def pattern(self, pattern: str) -> Iterator[config.ConfigContainer]:
"""Get a ConfigContainer for the given pattern."""
# We need to propagate the exception so we don't need to return
# something.
@@ -630,9 +636,8 @@ class ConfigPyWriter:
def __init__(
self,
- options: typing.List,
- bindings: typing.MutableMapping[
- str, typing.Mapping[str, typing.Optional[str]]],
+ options: List,
+ bindings: MutableMapping[str, Mapping[str, Optional[str]]],
*, commented: bool) -> None:
self._options = options
self._bindings = bindings
@@ -653,7 +658,7 @@ class ConfigPyWriter:
else:
return line
- def _gen_lines(self) -> typing.Iterator[str]:
+ def _gen_lines(self) -> Iterator[str]:
"""Generate a config.py with the given settings/bindings.
Yields individual lines.
@@ -662,7 +667,7 @@ class ConfigPyWriter:
yield from self._gen_options()
yield from self._gen_bindings()
- def _gen_header(self) -> typing.Iterator[str]:
+ def _gen_header(self) -> Iterator[str]:
"""Generate the initial header of the config."""
yield self._line("# Autogenerated config.py")
yield self._line("#")
@@ -687,15 +692,15 @@ class ConfigPyWriter:
"still loaded.")
yield self._line("# Remove it to not load settings done via the "
"GUI.")
- yield self._line("config.load_autoconfig()")
+ yield self._line("config.load_autoconfig(True)")
yield ''
else:
- yield self._line("# Uncomment this to still load settings "
+ yield self._line("# Change the argument to True to still load settings "
"configured via autoconfig.yml")
- yield self._line("# config.load_autoconfig()")
+ yield self._line("config.load_autoconfig(False)")
yield ''
- def _gen_options(self) -> typing.Iterator[str]:
+ def _gen_options(self) -> Iterator[str]:
"""Generate the options part of the config."""
for pattern, opt, value in self._options:
if opt.name in ['bindings.commands', 'bindings.default']:
@@ -723,7 +728,7 @@ class ConfigPyWriter:
opt.name, value, str(pattern)))
yield ''
- def _gen_bindings(self) -> typing.Iterator[str]:
+ def _gen_bindings(self) -> Iterator[str]:
"""Generate the bindings part of the config."""
normal_bindings = self._bindings.pop('normal', {})
if normal_bindings:
@@ -824,7 +829,7 @@ def read_autoconfig() -> None:
@contextlib.contextmanager
-def saved_sys_properties() -> typing.Iterator[None]:
+def saved_sys_properties() -> Iterator[None]:
"""Save various sys properties such as sys.path and sys.modules."""
old_path = sys.path.copy()
old_modules = sys.modules.copy()
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index 75148947e..6328c3140 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -47,17 +47,17 @@ import html
import codecs
import os.path
import itertools
-import warnings
import functools
import operator
import json
-import typing
+from typing import (Any, Callable, Dict as DictType, Iterable, Iterator,
+ List as ListType, Optional, Pattern, Sequence, Tuple, Union)
import attr
import yaml
from PyQt5.QtCore import QUrl, Qt
-from PyQt5.QtGui import QColor, QFontDatabase
-from PyQt5.QtWidgets import QTabWidget, QTabBar, QApplication
+from PyQt5.QtGui import QColor
+from PyQt5.QtWidgets import QTabWidget, QTabBar
from PyQt5.QtNetwork import QNetworkProxy
from qutebrowser.misc import objects, debugcachestats
@@ -80,10 +80,10 @@ BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
-_Completions = typing.Optional[typing.Iterable[typing.Tuple[str, str]]]
-_StrUnset = typing.Union[str, usertypes.Unset]
-_UnsetNone = typing.Union[None, usertypes.Unset]
-_StrUnsetNone = typing.Union[str, _UnsetNone]
+_Completions = Optional[Iterable[Tuple[str, str]]]
+_StrUnset = Union[str, usertypes.Unset]
+_UnsetNone = Union[None, usertypes.Unset]
+_StrUnsetNone = Union[str, _UnsetNone]
class ValidValues:
@@ -96,35 +96,40 @@ class ValidValues:
generate_docs: Whether to show the values in the docs.
"""
- def __init__(self,
- *values: typing.Union[str,
- typing.Dict[str, str],
- typing.Tuple[str, str]],
- generate_docs: bool = True) -> None:
+ def __init__(
+ self,
+ *values: Union[
+ str,
+ DictType[str, Optional[str]],
+ Tuple[str, Optional[str]],
+ ],
+ generate_docs: bool = True,
+ ) -> None:
if not values:
raise ValueError("ValidValues with no values makes no sense!")
- self.descriptions = {} # type: typing.Dict[str, str]
- self.values = [] # type: typing.List[str]
+ self.descriptions: DictType[str, str] = {}
+ self.values: ListType[str] = []
self.generate_docs = generate_docs
for value in values:
if isinstance(value, str):
# Value without description
- self.values.append(value)
+ val = value
+ desc = None
elif isinstance(value, dict):
# List of dicts from configdata.yml
assert len(value) == 1, value
- value, desc = list(value.items())[0]
- self.values.append(value)
- self.descriptions[value] = desc
+ val, desc = list(value.items())[0]
else:
- # (value, description) tuple
- self.values.append(value[0])
- self.descriptions[value[0]] = value[1]
+ val, desc = value
+
+ self.values.append(val)
+ if desc is not None:
+ self.descriptions[val] = desc
def __contains__(self, val: str) -> bool:
return val in self.values
- def __iter__(self) -> typing.Iterator[str]:
+ def __iter__(self) -> Iterator[str]:
return self.values.__iter__()
def __repr__(self) -> str:
@@ -151,19 +156,19 @@ class BaseType:
def __init__(self, none_ok: bool = False) -> None:
self.none_ok = none_ok
- self.valid_values = None # type: typing.Optional[ValidValues]
+ self.valid_values: Optional[ValidValues] = None
def get_name(self) -> str:
"""Get a name for the type for documentation."""
return self.__class__.__name__
- def get_valid_values(self) -> typing.Optional[ValidValues]:
+ def get_valid_values(self) -> Optional[ValidValues]:
"""Get the type's valid values for documentation."""
return self.valid_values
def _basic_py_validation(
- self, value: typing.Any,
- pytype: typing.Union[type, typing.Tuple[type, ...]]) -> None:
+ self, value: Any,
+ pytype: Union[type, Tuple[type, ...]]) -> None:
"""Do some basic validation for Python values (emptyness, type).
Arguments:
@@ -215,8 +220,7 @@ class BaseType:
raise configexc.ValidationError(
value, "may not contain unprintable chars!")
- def _validate_surrogate_escapes(self, full_value: typing.Any,
- value: typing.Any) -> None:
+ def _validate_surrogate_escapes(self, full_value: Any, value: Any) -> None:
"""Make sure the given value doesn't contain surrogate escapes.
This is used for values passed to json.dump, as it can't handle those.
@@ -242,7 +246,7 @@ class BaseType:
value,
"valid values: {}".format(', '.join(self.valid_values)))
- def from_str(self, value: str) -> typing.Any:
+ def from_str(self, value: str) -> Any:
"""Get the setting value from a string.
By default this invokes to_py() for validation and returns the
@@ -261,11 +265,11 @@ class BaseType:
return None
return value
- def from_obj(self, value: typing.Any) -> typing.Any:
+ def from_obj(self, value: Any) -> Any:
"""Get the setting value from a config.py/YAML object."""
return value
- def to_py(self, value: typing.Any) -> typing.Any:
+ def to_py(self, value: Any) -> Any:
"""Get the setting value from a Python value.
Args:
@@ -279,7 +283,7 @@ class BaseType:
"""
raise NotImplementedError
- def to_str(self, value: typing.Any) -> str:
+ def to_str(self, value: Any) -> str:
"""Get a string from the setting value.
The resulting string should be parseable again by from_str.
@@ -289,7 +293,7 @@ class BaseType:
assert isinstance(value, str), value
return value
- def to_doc(self, value: typing.Any, indent: int = 0) -> str:
+ def to_doc(self, value: Any, indent: int = 0) -> str:
"""Get a string with the given value for the documentation.
This currently uses asciidoc syntax.
@@ -311,17 +315,10 @@ class BaseType:
"""
if self.valid_values is None:
return None
- else:
- out = []
- for val in self.valid_values:
- try:
- desc = self.valid_values.descriptions[val]
- except KeyError:
- # Some values are self-explaining and don't need a
- # description.
- desc = ""
- out.append((val, desc))
- return out
+ return [
+ (val, self.valid_values.descriptions.get(val, ""))
+ for val in self.valid_values
+ ]
def __repr__(self) -> str:
return utils.get_repr(self, none_ok=self.none_ok)
@@ -332,24 +329,25 @@ class MappingType(BaseType):
"""Base class for any setting which has a mapping to the given values.
Attributes:
- MAPPING: The mapping to use.
+ MAPPING: A mapping from config values to (translated_value, docs) tuples.
"""
- MAPPING = {} # type: typing.Dict[str, typing.Any]
+ MAPPING: DictType[str, Tuple[Any, Optional[str]]] = {}
- def __init__(self, none_ok: bool = False,
- valid_values: ValidValues = None) -> None:
+ def __init__(self, none_ok: bool = False) -> None:
super().__init__(none_ok)
- self.valid_values = valid_values
+ self.valid_values = ValidValues(
+ *[(key, doc) for (key, (_val, doc)) in self.MAPPING.items()])
- def to_py(self, value: typing.Any) -> typing.Any:
+ def to_py(self, value: Any) -> Any:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
self._validate_valid_values(value.lower())
- return self.MAPPING[value.lower()]
+ mapped, _doc = self.MAPPING[value.lower()]
+ return mapped
def __repr__(self) -> str:
return utils.get_repr(self, none_ok=self.none_ok,
@@ -492,10 +490,10 @@ class List(BaseType):
name += " of " + self.valtype.get_name()
return name
- def get_valid_values(self) -> typing.Optional[ValidValues]:
+ def get_valid_values(self) -> Optional[ValidValues]:
return self.valtype.get_valid_values()
- def from_str(self, value: str) -> typing.Optional[typing.List]:
+ def from_str(self, value: str) -> Optional[ListType]:
self._basic_str_validation(value)
if not value:
return None
@@ -510,15 +508,15 @@ class List(BaseType):
self.to_py(yaml_val)
return yaml_val
- def from_obj(self, value: typing.Optional[typing.List]) -> typing.List:
+ def from_obj(self, value: Optional[ListType]) -> ListType:
if value is None:
return []
return [self.valtype.from_obj(v) for v in value]
def to_py(
self,
- value: typing.Union[typing.List, usertypes.Unset]
- ) -> typing.Union[typing.List, usertypes.Unset]:
+ value: Union[ListType, usertypes.Unset]
+ ) -> Union[ListType, usertypes.Unset]:
self._basic_py_validation(value, list)
if isinstance(value, usertypes.Unset):
return value
@@ -533,13 +531,13 @@ class List(BaseType):
"be set!".format(self.length))
return [self.valtype.to_py(v) for v in value]
- def to_str(self, value: typing.List) -> str:
+ def to_str(self, value: ListType) -> str:
if not value:
# An empty list is treated just like None -> empty string
return ''
return json.dumps(value)
- def to_doc(self, value: typing.List, indent: int = 0) -> str:
+ def to_doc(self, value: ListType, indent: int = 0) -> str:
if not value:
return 'empty'
@@ -573,14 +571,13 @@ class ListOrValue(BaseType):
def __init__(self, valtype: BaseType, *,
none_ok: bool = False,
- **kwargs: typing.Any) -> None:
+ **kwargs: Any) -> None:
super().__init__(none_ok)
assert not isinstance(valtype, (List, ListOrValue)), valtype
self.listtype = List(valtype, none_ok=none_ok, **kwargs)
self.valtype = valtype
- def _val_and_type(self,
- value: typing.Any) -> typing.Tuple[typing.Any, BaseType]:
+ def _val_and_type(self, value: Any) -> Tuple[Any, BaseType]:
"""Get the value and type to use for to_str/to_doc/from_str."""
if isinstance(value, list):
if len(value) == 1:
@@ -593,21 +590,21 @@ class ListOrValue(BaseType):
def get_name(self) -> str:
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
- def get_valid_values(self) -> typing.Optional[ValidValues]:
+ def get_valid_values(self) -> Optional[ValidValues]:
return self.valtype.get_valid_values()
- def from_str(self, value: str) -> typing.Any:
+ def from_str(self, value: str) -> Any:
try:
return self.listtype.from_str(value)
except configexc.ValidationError:
return self.valtype.from_str(value)
- def from_obj(self, value: typing.Any) -> typing.Any:
+ def from_obj(self, value: Any) -> Any:
if value is None:
return []
return value
- def to_py(self, value: typing.Any) -> typing.Any:
+ def to_py(self, value: Any) -> Any:
if isinstance(value, usertypes.Unset):
return value
@@ -616,14 +613,14 @@ class ListOrValue(BaseType):
except configexc.ValidationError:
return self.listtype.to_py(value)
- def to_str(self, value: typing.Any) -> str:
+ def to_str(self, value: Any) -> str:
if value is None:
return ''
val, typ = self._val_and_type(value)
return typ.to_str(val)
- def to_doc(self, value: typing.Any, indent: int = 0) -> str:
+ def to_doc(self, value: Any, indent: int = 0) -> str:
if value is None:
return 'empty'
@@ -642,7 +639,7 @@ class FlagList(List):
the valid values of the setting.
"""
- combinable_values = None # type: typing.Optional[typing.Sequence]
+ combinable_values: Optional[Sequence] = None
_show_valtype = False
@@ -652,15 +649,15 @@ class FlagList(List):
super().__init__(valtype=String(), none_ok=none_ok, length=length)
self.valtype.valid_values = valid_values
- def _check_duplicates(self, values: typing.List) -> None:
+ def _check_duplicates(self, values: ListType) -> None:
if len(set(values)) != len(values):
raise configexc.ValidationError(
values, "List contains duplicate values!")
def to_py(
self,
- value: typing.Union[usertypes.Unset, typing.List],
- ) -> typing.Union[usertypes.Unset, typing.List]:
+ value: Union[usertypes.Unset, ListType],
+ ) -> Union[usertypes.Unset, ListType]:
vals = super().to_py(value)
if not isinstance(vals, usertypes.Unset):
self._check_duplicates(vals)
@@ -704,13 +701,12 @@ class Bool(BaseType):
super().__init__(none_ok)
self.valid_values = ValidValues('true', 'false', generate_docs=False)
- def to_py(self,
- value: typing.Union[bool, str, None]) -> typing.Optional[bool]:
+ def to_py(self, value: Union[bool, str, None]) -> Optional[bool]:
self._basic_py_validation(value, bool)
assert not isinstance(value, str)
return value
- def from_str(self, value: str) -> typing.Optional[bool]:
+ def from_str(self, value: str) -> Optional[bool]:
self._basic_str_validation(value)
if not value:
return None
@@ -720,7 +716,7 @@ class Bool(BaseType):
except KeyError:
raise configexc.ValidationError(value, "must be a boolean!")
- def to_str(self, value: typing.Optional[bool]) -> str:
+ def to_str(self, value: Optional[bool]) -> str:
mapping = {
None: '',
True: 'true',
@@ -738,7 +734,7 @@ class BoolAsk(Bool):
self.valid_values = ValidValues('true', 'false', 'ask')
def to_py(self, # type: ignore[override]
- value: typing.Union[bool, str]) -> typing.Union[bool, str, None]:
+ value: Union[bool, str]) -> Union[bool, str, None]:
# basic validation unneeded if it's == 'ask' and done by Bool if we
# call super().to_py
if isinstance(value, str) and value.lower() == 'ask':
@@ -746,14 +742,14 @@ class BoolAsk(Bool):
return super().to_py(value)
def from_str(self, # type: ignore[override]
- value: str) -> typing.Union[bool, str, None]:
+ value: str) -> Union[bool, str, None]:
# basic validation unneeded if it's == 'ask' and done by Bool if we
# call super().from_str
if value.lower() == 'ask':
return 'ask'
return super().from_str(value)
- def to_str(self, value: typing.Union[bool, str, None]) -> str:
+ def to_str(self, value: Union[bool, str, None]) -> str:
mapping = {
None: '',
True: 'true',
@@ -786,8 +782,8 @@ class _Numeric(BaseType): # pylint: disable=abstract-method
.format(self.minval, self.maxval))
def _parse_bound(
- self, bound: typing.Union[None, str, int, float]
- ) -> typing.Union[None, int, float]:
+ self, bound: Union[None, str, int, float]
+ ) -> Union[None, int, float]:
"""Get a numeric bound from a string like 'maxint'."""
if bound == 'maxint':
return qtutils.MAXVALS['int']
@@ -799,7 +795,7 @@ class _Numeric(BaseType): # pylint: disable=abstract-method
return bound
def _validate_bounds(self,
- value: typing.Union[int, float, _UnsetNone],
+ value: Union[int, float, _UnsetNone],
suffix: str = '') -> None:
"""Validate self.minval and self.maxval."""
if value is None:
@@ -815,7 +811,7 @@ class _Numeric(BaseType): # pylint: disable=abstract-method
elif not self.zero_ok and value == 0:
raise configexc.ValidationError(value, "must not be 0!")
- def to_str(self, value: typing.Union[None, int, float]) -> str:
+ def to_str(self, value: Union[None, int, float]) -> str:
if value is None:
return ''
return str(value)
@@ -829,7 +825,7 @@ class Int(_Numeric):
"""Base class for an integer setting."""
- def from_str(self, value: str) -> typing.Optional[int]:
+ def from_str(self, value: str) -> Optional[int]:
self._basic_str_validation(value)
if not value:
return None
@@ -841,10 +837,7 @@ class Int(_Numeric):
self.to_py(intval)
return intval
- def to_py(
- self,
- value: typing.Union[int, _UnsetNone]
- ) -> typing.Union[int, _UnsetNone]:
+ def to_py(self, value: Union[int, _UnsetNone]) -> Union[int, _UnsetNone]:
self._basic_py_validation(value, int)
self._validate_bounds(value)
return value
@@ -854,7 +847,7 @@ class Float(_Numeric):
"""Base class for a float setting."""
- def from_str(self, value: str) -> typing.Optional[float]:
+ def from_str(self, value: str) -> Optional[float]:
self._basic_str_validation(value)
if not value:
return None
@@ -868,8 +861,8 @@ class Float(_Numeric):
def to_py(
self,
- value: typing.Union[int, float, _UnsetNone],
- ) -> typing.Union[int, float, _UnsetNone]:
+ value: Union[int, float, _UnsetNone],
+ ) -> Union[int, float, _UnsetNone]:
self._basic_py_validation(value, (int, float))
self._validate_bounds(value)
return value
@@ -881,8 +874,8 @@ class Perc(_Numeric):
def to_py(
self,
- value: typing.Union[float, int, str, _UnsetNone]
- ) -> typing.Union[float, int, _UnsetNone]:
+ value: Union[float, int, str, _UnsetNone]
+ ) -> Union[float, int, _UnsetNone]:
self._basic_py_validation(value, (float, int, str))
if isinstance(value, usertypes.Unset):
return value
@@ -899,7 +892,7 @@ class Perc(_Numeric):
self._validate_bounds(value, suffix='%')
return value
- def to_str(self, value: typing.Union[None, float, int, str]) -> str:
+ def to_str(self, value: Union[None, float, int, str]) -> str:
if value is None:
return ''
elif isinstance(value, str):
@@ -930,7 +923,7 @@ class PercOrInt(_Numeric):
raise ValueError("minperc ({}) needs to be <= maxperc "
"({})!".format(self.minperc, self.maxperc))
- def from_str(self, value: str) -> typing.Union[None, str, int]:
+ def from_str(self, value: str) -> Union[None, str, int]:
self._basic_str_validation(value)
if not value:
return None
@@ -947,10 +940,7 @@ class PercOrInt(_Numeric):
self.to_py(intval)
return intval
- def to_py(
- self,
- value: typing.Union[None, str, int]
- ) -> typing.Union[None, str, int]:
+ def to_py(self, value: Union[None, str, int]) -> Union[None, str, int]:
"""Expect a value like '42%' as string, or 23 as int."""
self._basic_py_validation(value, (int, str))
if value is None:
@@ -1010,20 +1000,11 @@ class ColorSystem(MappingType):
"""The color system to use for color interpolation."""
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues(
- ('rgb', "Interpolate in the RGB color system."),
- ('hsv', "Interpolate in the HSV color system."),
- ('hsl', "Interpolate in the HSL color system."),
- ('none', "Don't show a gradient.")))
-
MAPPING = {
- 'rgb': QColor.Rgb,
- 'hsv': QColor.Hsv,
- 'hsl': QColor.Hsl,
- 'none': None,
+ 'rgb': (QColor.Rgb, "Interpolate in the RGB color system."),
+ 'hsv': (QColor.Hsv, "Interpolate in the HSV color system."),
+ 'hsl': (QColor.Hsl, "Interpolate in the HSL color system."),
+ 'none': (None, "Don't show a gradient."),
}
@@ -1031,19 +1012,13 @@ class IgnoreCase(MappingType):
"""Whether to search case insensitively."""
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues(
- ('always', "Search case-insensitively."),
- ('never', "Search case-sensitively."),
- ('smart', ("Search case-sensitively if there are capital "
- "characters."))))
-
MAPPING = {
- 'always': usertypes.IgnoreCase.always,
- 'never': usertypes.IgnoreCase.never,
- 'smart': usertypes.IgnoreCase.smart,
+ 'always': (usertypes.IgnoreCase.always, "Search case-insensitively."),
+ 'never': (usertypes.IgnoreCase.never, "Search case-sensitively."),
+ 'smart': (
+ usertypes.IgnoreCase.smart,
+ "Search case-sensitively if there are capital characters."
+ ),
}
@@ -1077,7 +1052,7 @@ class QtColor(BaseType):
except ValueError:
raise configexc.ValidationError(val, "must be a valid color value")
- def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QColor]:
+ def to_py(self, value: _StrUnset) -> Union[_UnsetNone, QColor]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@@ -1089,12 +1064,12 @@ class QtColor(BaseType):
kind = value[:openparen]
vals = value[openparen+1:-1].split(',')
- converters = {
+ converters: DictType[str, Callable[..., QColor]] = {
'rgba': QColor.fromRgb,
'rgb': QColor.fromRgb,
'hsva': QColor.fromHsv,
'hsv': QColor.fromHsv,
- } # type: typing.Dict[str, typing.Callable[..., QColor]]
+ }
conv = converters.get(kind)
if not conv:
@@ -1160,8 +1135,8 @@ class FontBase(BaseType):
"""Base class for Font/FontFamily."""
# Gets set when the config is initialized.
- default_family = None # type: str
- default_size = None # type: str
+ default_family: Optional[str] = None
+ default_size: Optional[str] = None
font_regex = re.compile(r"""
(
(
@@ -1179,61 +1154,21 @@ class FontBase(BaseType):
(?P<family>.+) # mandatory font family""", re.VERBOSE)
@classmethod
- def set_defaults(cls, default_family: typing.List[str],
- default_size: str) -> None:
+ def set_defaults(cls, default_family: ListType[str], default_size: str) -> None:
"""Make sure default_family/default_size are available.
If the given family value (fonts.default_family in the config) is
unset, a system-specific default monospace font is used.
-
- Note that (at least) three ways of getting the default monospace font
- exist:
-
- 1) f = QFont()
- f.setStyleHint(QFont.Monospace)
- print(f.defaultFamily())
-
- 2) f = QFont()
- f.setStyleHint(QFont.TypeWriter)
- print(f.defaultFamily())
-
- 3) f = QFontDatabase.systemFont(QFontDatabase.FixedFont)
- print(f.family())
-
- They yield different results depending on the OS:
-
- QFont.Monospace | QFont.TypeWriter | QFontDatabase
- ------------------------------------------------------
- Windows: Courier New | Courier New | Courier New
- Linux: DejaVu Sans Mono | DejaVu Sans Mono | monospace
- macOS: Menlo | American Typewriter | Monaco
-
- Test script: https://p.cmpl.cc/d4dfe573
-
- On Linux, it seems like both actually resolve to the same font.
-
- On macOS, "American Typewriter" looks like it indeed tries to imitate a
- typewriter, so it's not really a suitable UI font.
-
- Looking at those Wikipedia articles:
-
- https://en.wikipedia.org/wiki/Monaco_(typeface)
- https://en.wikipedia.org/wiki/Menlo_(typeface)
-
- the "right" choice isn't really obvious. Thus, let's go for the
- QFontDatabase approach here, since it's by far the simplest one.
"""
if default_family:
families = configutils.FontFamilies(default_family)
else:
- assert QApplication.instance() is not None
- font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
- families = configutils.FontFamilies([font.family()])
+ families = configutils.FontFamilies.from_system_default()
cls.default_family = families.to_str(quote=True)
cls.default_size = default_size
- def to_py(self, value: typing.Any) -> typing.Any:
+ def to_py(self, value: Any) -> Any:
raise NotImplementedError
@@ -1316,40 +1251,29 @@ class Regex(BaseType):
operator.or_,
(getattr(re, flag.strip()) for flag in flags.split(' | ')))
- def _compile_regex(self, pattern: str) -> typing.Pattern[str]:
+ def _compile_regex(self, pattern: str) -> Pattern[str]:
"""Check if the given regex is valid.
- This is more complicated than it could be since there's a warning on
- invalid escapes with newer Python versions, and we want to catch that
- case and treat it as invalid.
+ Some semi-invalid regexes can also raise warnings - we also treat them as
+ invalid.
"""
- with warnings.catch_warnings(record=True) as recorded_warnings:
- warnings.simplefilter('always')
- try:
+ try:
+ with log.py_warning_filter('error', category=FutureWarning):
compiled = re.compile(pattern, self.flags)
- except re.error as e:
- raise configexc.ValidationError(
- pattern, "must be a valid regex - " + str(e))
- except RuntimeError: # pragma: no cover
- raise configexc.ValidationError(
- pattern, "must be a valid regex - recursion depth "
- "exceeded")
-
- assert recorded_warnings is not None
-
- for w in recorded_warnings:
- if (issubclass(w.category, DeprecationWarning) and
- str(w.message).startswith('bad escape')):
- raise configexc.ValidationError(
- pattern, "must be a valid regex - " + str(w.message))
- warnings.warn(w.message)
+ except (re.error, FutureWarning) as e:
+ raise configexc.ValidationError(
+ pattern, "must be a valid regex - " + str(e))
+ except RuntimeError: # pragma: no cover
+ raise configexc.ValidationError(
+ pattern, "must be a valid regex - recursion depth "
+ "exceeded")
return compiled
def to_py(
self,
- value: typing.Union[str, typing.Pattern[str], usertypes.Unset]
- ) -> typing.Union[_UnsetNone, typing.Pattern[str]]:
+ value: Union[str, Pattern[str], usertypes.Unset]
+ ) -> Union[_UnsetNone, Pattern[str]]:
"""Get a compiled regex from either a string or a regex object."""
self._basic_py_validation(value, (str, self._regex_type))
if isinstance(value, usertypes.Unset):
@@ -1361,8 +1285,7 @@ class Regex(BaseType):
else:
return value
- def to_str(self,
- value: typing.Union[None, str, typing.Pattern[str]]) -> str:
+ def to_str(self, value: Union[None, str, Pattern[str]]) -> str:
if value is None:
return ''
elif isinstance(value, self._regex_type):
@@ -1382,10 +1305,10 @@ class Dict(BaseType):
When setting from a string, pass a json-like dict, e.g. `{"key", "value"}`.
"""
- def __init__(self, keytype: typing.Union[String, 'Key'],
+ def __init__(self, keytype: Union[String, 'Key'],
valtype: BaseType, *,
- fixed_keys: typing.Iterable = None,
- required_keys: typing.Iterable = None,
+ fixed_keys: Iterable = None,
+ required_keys: Iterable = None,
none_ok: bool = False) -> None:
super().__init__(none_ok)
# If the keytype is not a string, we'll get problems with showing it as
@@ -1396,7 +1319,7 @@ class Dict(BaseType):
self.fixed_keys = fixed_keys
self.required_keys = required_keys
- def _validate_keys(self, value: typing.Dict) -> None:
+ def _validate_keys(self, value: DictType) -> None:
if (self.fixed_keys is not None and not
set(value.keys()).issubset(self.fixed_keys)):
raise configexc.ValidationError(
@@ -1407,7 +1330,7 @@ class Dict(BaseType):
raise configexc.ValidationError(
value, "Required keys {}".format(self.required_keys))
- def from_str(self, value: str) -> typing.Optional[typing.Dict]:
+ def from_str(self, value: str) -> Optional[DictType]:
self._basic_str_validation(value)
if not value:
return None
@@ -1422,14 +1345,14 @@ class Dict(BaseType):
self.to_py(yaml_val)
return yaml_val
- def from_obj(self, value: typing.Optional[typing.Dict]) -> typing.Dict:
+ def from_obj(self, value: Optional[DictType]) -> DictType:
if value is None:
return {}
return {self.keytype.from_obj(key): self.valtype.from_obj(val)
for key, val in value.items()}
- def _fill_fixed_keys(self, value: typing.Dict) -> typing.Dict:
+ def _fill_fixed_keys(self, value: DictType) -> DictType:
"""Fill missing fixed keys with a None-value."""
if self.fixed_keys is None:
return value
@@ -1440,8 +1363,8 @@ class Dict(BaseType):
def to_py(
self,
- value: typing.Union[typing.Dict, _UnsetNone]
- ) -> typing.Union[typing.Dict, usertypes.Unset]:
+ value: Union[DictType, _UnsetNone]
+ ) -> Union[DictType, usertypes.Unset]:
self._basic_py_validation(value, dict)
if isinstance(value, usertypes.Unset):
return value
@@ -1457,13 +1380,13 @@ class Dict(BaseType):
for key, val in value.items()}
return self._fill_fixed_keys(d)
- def to_str(self, value: typing.Dict) -> str:
+ def to_str(self, value: DictType) -> str:
if not value:
# An empty Dict is treated just like None -> empty string
return ''
return json.dumps(value, sort_keys=True)
- def to_doc(self, value: typing.Dict, indent: int = 0) -> str:
+ def to_doc(self, value: DictType, indent: int = 0) -> str:
if not value:
return 'empty'
lines = ['\n']
@@ -1486,7 +1409,7 @@ class File(BaseType):
"""A file on the local filesystem."""
- def __init__(self, required: bool = True, **kwargs: typing.Any) -> None:
+ def __init__(self, required: bool = True, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.required = required
@@ -1552,7 +1475,7 @@ class FormatString(BaseType):
completions: completions to be used, or None
"""
- def __init__(self, fields: typing.Iterable[str],
+ def __init__(self, fields: Iterable[str],
none_ok: bool = False,
completions: _Completions = None) -> None:
super().__init__(none_ok)
@@ -1605,8 +1528,8 @@ class ShellCommand(List):
def to_py(
self,
- value: typing.Union[typing.List, usertypes.Unset],
- ) -> typing.Union[typing.List, usertypes.Unset]:
+ value: Union[ListType, usertypes.Unset],
+ ) -> Union[ListType, usertypes.Unset]:
py_value = super().to_py(value)
if isinstance(py_value, usertypes.Unset):
return py_value
@@ -1639,7 +1562,7 @@ class Proxy(BaseType):
def to_py(
self,
value: _StrUnset
- ) -> typing.Union[_UnsetNone, QNetworkProxy, _SystemProxy, pac.PACFetcher]:
+ ) -> Union[_UnsetNone, QNetworkProxy, _SystemProxy, pac.PACFetcher]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@@ -1709,7 +1632,7 @@ class FuzzyUrl(BaseType):
"""A URL which gets interpreted as search if needed."""
- def to_py(self, value: _StrUnset) -> typing.Union[QUrl, _UnsetNone]:
+ def to_py(self, value: _StrUnset) -> Union[QUrl, _UnsetNone]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@@ -1727,10 +1650,10 @@ class PaddingValues:
"""Four padding values."""
- top = attr.ib() # type: int
- bottom = attr.ib() # type: int
- left = attr.ib() # type: int
- right = attr.ib() # type: int
+ top: int = attr.ib()
+ bottom: int = attr.ib()
+ left: int = attr.ib()
+ right: int = attr.ib()
class Padding(Dict):
@@ -1747,8 +1670,8 @@ class Padding(Dict):
def to_py( # type: ignore[override]
self,
- value: typing.Union[typing.Dict, _UnsetNone],
- ) -> typing.Union[usertypes.Unset, PaddingValues]:
+ value: Union[DictType, _UnsetNone],
+ ) -> Union[usertypes.Unset, PaddingValues]:
d = super().to_py(value)
if isinstance(d, usertypes.Unset):
return d
@@ -1778,33 +1701,23 @@ class Position(MappingType):
"""The position of the tab bar."""
MAPPING = {
- 'top': QTabWidget.North,
- 'bottom': QTabWidget.South,
- 'left': QTabWidget.West,
- 'right': QTabWidget.East,
+ 'top': (QTabWidget.North, None),
+ 'bottom': (QTabWidget.South, None),
+ 'left': (QTabWidget.West, None),
+ 'right': (QTabWidget.East, None),
}
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues('top', 'bottom', 'left', 'right'))
-
class TextAlignment(MappingType):
"""Alignment of text."""
MAPPING = {
- 'left': Qt.AlignLeft,
- 'right': Qt.AlignRight,
- 'center': Qt.AlignCenter,
+ 'left': (Qt.AlignLeft, None),
+ 'right': (Qt.AlignRight, None),
+ 'center': (Qt.AlignCenter, None),
}
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues('left', 'right', 'center'))
-
class VerticalPosition(String):
@@ -1819,7 +1732,7 @@ class Url(BaseType):
"""A URL as a string."""
- def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QUrl]:
+ def to_py(self, value: _StrUnset) -> Union[_UnsetNone, QUrl]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@@ -1853,21 +1766,22 @@ class SelectOnRemove(MappingType):
"""Which tab to select when the focused tab is removed."""
MAPPING = {
- 'prev': QTabBar.SelectLeftTab,
- 'next': QTabBar.SelectRightTab,
- 'last-used': QTabBar.SelectPreviousTab,
+ 'prev': (
+ QTabBar.SelectLeftTab,
+ ("Select the tab which came before the closed one "
+ "(left in horizontal, above in vertical)."),
+ ),
+ 'next': (
+ QTabBar.SelectRightTab,
+ ("Select the tab which came after the closed one "
+ "(right in horizontal, below in vertical)."),
+ ),
+ 'last-used': (
+ QTabBar.SelectPreviousTab,
+ "Select the previously selected tab.",
+ ),
}
- def __init__(self, none_ok: bool = False) -> None:
- super().__init__(
- none_ok,
- valid_values=ValidValues(
- ('prev', "Select the tab which came before the closed one "
- "(left in horizontal, above in vertical)."),
- ('next', "Select the tab which came after the closed one "
- "(right in horizontal, below in vertical)."),
- ('last-used', "Select the previously selected tab.")))
-
class ConfirmQuit(FlagList):
@@ -1889,8 +1803,8 @@ class ConfirmQuit(FlagList):
def to_py(
self,
- value: typing.Union[usertypes.Unset, typing.List],
- ) -> typing.Union[typing.List, usertypes.Unset]:
+ value: Union[usertypes.Unset, ListType],
+ ) -> Union[ListType, usertypes.Unset]:
values = super().to_py(value)
if isinstance(values, usertypes.Unset):
return values
@@ -1943,7 +1857,7 @@ class Key(BaseType):
def to_py(
self,
value: _StrUnset
- ) -> typing.Union[_UnsetNone, keyutils.KeySequence]:
+ ) -> Union[_UnsetNone, keyutils.KeySequence]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
@@ -1967,7 +1881,7 @@ class UrlPattern(BaseType):
def to_py(
self,
value: _StrUnset
- ) -> typing.Union[_UnsetNone, urlmatch.UrlPattern]:
+ ) -> Union[_UnsetNone, urlmatch.UrlPattern]:
self._basic_py_validation(value, str)
if isinstance(value, usertypes.Unset):
return value
diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py
index 3f7823772..e7a60a7eb 100644
--- a/qutebrowser/config/configutils.py
+++ b/qutebrowser/config/configutils.py
@@ -21,21 +21,25 @@
"""Utilities and data structures used by various config code."""
-import typing
import collections
import itertools
import operator
+from typing import (
+ TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Union,
+ MutableMapping)
from PyQt5.QtCore import QUrl
+from PyQt5.QtGui import QFontDatabase
+from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import utils, urlmatch, usertypes, qtutils
from qutebrowser.config import configexc
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.config import configdata
-def _widened_hostnames(hostname: str) -> typing.Iterable[str]:
+def _widened_hostnames(hostname: str) -> Iterable[str]:
"""A generator for widening string hostnames.
Ex: a.c.foo -> [a.c.foo, c.foo, foo]"""
@@ -56,8 +60,8 @@ class ScopedValue:
id_gen = itertools.count(0)
- def __init__(self, value: typing.Any,
- pattern: typing.Optional[urlmatch.UrlPattern],
+ def __init__(self, value: Any,
+ pattern: Optional[urlmatch.UrlPattern],
hide_userconfig: bool = False) -> None:
self.value = value
self.pattern = pattern
@@ -90,17 +94,17 @@ class Values:
_domain_map: A mapping from hostnames to all associated ScopedValues.
"""
- _VmapKeyType = typing.Optional[urlmatch.UrlPattern]
+ _VmapKeyType = Optional[urlmatch.UrlPattern]
def __init__(self,
opt: 'configdata.Option',
- values: typing.Sequence[ScopedValue] = ()) -> None:
+ values: Sequence[ScopedValue] = ()) -> None:
self.opt = opt
- self._vmap = collections.OrderedDict() \
- # type: collections.OrderedDict[Values._VmapKeyType, ScopedValue]
+ self._vmap: MutableMapping[
+ Values._VmapKeyType, ScopedValue] = collections.OrderedDict()
# A map from domain parts to rules that fall under them.
- self._domain_map = collections.defaultdict(set) \
- # type: typing.Dict[typing.Optional[str], typing.Set[ScopedValue]]
+ self._domain_map: Dict[
+ Optional[str], Set[ScopedValue]] = collections.defaultdict(set)
for scoped in values:
self._add_scoped(scoped)
@@ -117,7 +121,7 @@ class Values:
return '\n'.join(lines)
return '{}: <unchanged>'.format(self.opt.name)
- def dump(self, include_hidden: bool = False) -> typing.Sequence[str]:
+ def dump(self, include_hidden: bool = False) -> Sequence[str]:
"""Dump all customizations for this value.
Arguments:
@@ -138,7 +142,7 @@ class Values:
return lines
- def __iter__(self) -> typing.Iterator['ScopedValue']:
+ def __iter__(self) -> Iterator['ScopedValue']:
"""Yield ScopedValue elements.
This yields in "normal" order, i.e. global and then first-set settings
@@ -151,12 +155,12 @@ class Values:
return bool(self._vmap)
def _check_pattern_support(
- self, arg: typing.Union[urlmatch.UrlPattern, QUrl, None]) -> None:
+ self, arg: Union[urlmatch.UrlPattern, QUrl, None]) -> None:
"""Make sure patterns are supported if one was given."""
if arg is not None and not self.opt.supports_pattern:
raise configexc.NoPatternError(self.opt.name)
- def add(self, value: typing.Any,
+ def add(self, value: Any,
pattern: urlmatch.UrlPattern = None, *,
hide_userconfig: bool = False) -> None:
"""Add a value with the given pattern to the list of values.
@@ -201,7 +205,7 @@ class Values:
self._vmap.clear()
self._domain_map.clear()
- def _get_fallback(self, fallback: bool) -> typing.Any:
+ def _get_fallback(self, fallback: bool) -> Any:
"""Get the fallback global/default value."""
if None in self._vmap:
return self._vmap[None].value
@@ -211,8 +215,7 @@ class Values:
else:
return usertypes.UNSET
- def get_for_url(self, url: QUrl = None, *,
- fallback: bool = True) -> typing.Any:
+ def get_for_url(self, url: QUrl = None, *, fallback: bool = True) -> Any:
"""Get a config value, falling back when needed.
This first tries to find a value matching the URL (if given).
@@ -225,7 +228,7 @@ class Values:
return self._get_fallback(fallback)
qtutils.ensure_valid(url)
- candidates = [] # type: typing.List[ScopedValue]
+ candidates: List[ScopedValue] = []
# Urls trailing with '.' are equivalent to non-trailing types.
# urlutils strips them, so in order to match we will need to as well.
widened_hosts = _widened_hostnames(url.host().rstrip('.'))
@@ -247,8 +250,8 @@ class Values:
return self._get_fallback(fallback)
def get_for_pattern(self,
- pattern: typing.Optional[urlmatch.UrlPattern], *,
- fallback: bool = True) -> typing.Any:
+ pattern: Optional[urlmatch.UrlPattern], *,
+ fallback: bool = True) -> Any:
"""Get a value only if it's been overridden for the given pattern.
This is useful when showing values to the user.
@@ -272,22 +275,25 @@ class FontFamilies:
"""A list of font family names."""
- def __init__(self, families: typing.Sequence[str]) -> None:
+ def __init__(self, families: Sequence[str]) -> None:
self._families = families
self.family = families[0] if families else None
- def __iter__(self) -> typing.Iterator[str]:
+ def __iter__(self) -> Iterator[str]:
yield from self._families
+ def __len__(self) -> int:
+ return len(self._families)
+
def __repr__(self) -> str:
return utils.get_repr(self, families=self._families, constructor=True)
def __str__(self) -> str:
return self.to_str()
- def _quoted_families(self) -> typing.Iterator[str]:
+ def _quoted_families(self) -> Iterator[str]:
for f in self._families:
- needs_quoting = any(c in f for c in ', ')
+ needs_quoting = any(c in f for c in '., ')
yield '"{}"'.format(f) if needs_quoting else f
def to_str(self, *, quote: bool = True) -> str:
@@ -295,6 +301,57 @@ class FontFamilies:
return ', '.join(families)
@classmethod
+ def from_system_default(
+ cls,
+ font_type: QFontDatabase.SystemFont = QFontDatabase.FixedFont,
+ ) -> 'FontFamilies':
+ """Get a FontFamilies object for the default system font.
+
+ By default, the monospace font is returned, though via the "font_type" argument,
+ other types can be requested as well.
+
+ Note that (at least) three ways of getting the default monospace font
+ exist:
+
+ 1) f = QFont()
+ f.setStyleHint(QFont.Monospace)
+ print(f.defaultFamily())
+
+ 2) f = QFont()
+ f.setStyleHint(QFont.TypeWriter)
+ print(f.defaultFamily())
+
+ 3) f = QFontDatabase.systemFont(QFontDatabase.FixedFont)
+ print(f.family())
+
+ They yield different results depending on the OS:
+
+ QFont.Monospace | QFont.TypeWriter | QFontDatabase
+ ------------------------------------------------------
+ Windows: Courier New | Courier New | Courier New
+ Linux: DejaVu Sans Mono | DejaVu Sans Mono | monospace
+ macOS: Menlo | American Typewriter | Monaco
+
+ Test script: https://p.cmpl.cc/d4dfe573
+
+ On Linux, it seems like both actually resolve to the same font.
+
+ On macOS, "American Typewriter" looks like it indeed tries to imitate a
+ typewriter, so it's not really a suitable UI font.
+
+ Looking at those Wikipedia articles:
+
+ https://en.wikipedia.org/wiki/Monaco_(typeface)
+ https://en.wikipedia.org/wiki/Menlo_(typeface)
+
+ the "right" choice isn't really obvious. Thus, let's go for the
+ QFontDatabase approach here, since it's by far the simplest one.
+ """
+ assert QApplication.instance() is not None
+ font = QFontDatabase.systemFont(font_type)
+ return cls([font.family()])
+
+ @classmethod
def from_str(cls, family_str: str) -> 'FontFamilies':
"""Parse a CSS-like string of font families."""
families = []
diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py
index 0c517a14c..2136f7e7f 100644
--- a/qutebrowser/config/qtargs.py
+++ b/qutebrowser/config/qtargs.py
@@ -21,15 +21,15 @@
import os
import sys
-import typing
import argparse
+from typing import Any, Dict, Iterator, List, Optional, Sequence
from qutebrowser.config import config
from qutebrowser.misc import objects
from qutebrowser.utils import usertypes, qtutils, utils
-def qt_args(namespace: argparse.Namespace) -> typing.List[str]:
+def qt_args(namespace: argparse.Namespace) -> List[str]:
"""Get the Qt QApplication arguments based on an argparse namespace.
Args:
@@ -61,93 +61,7 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]:
return argv
-def _darkmode_settings() -> typing.Iterator[typing.Tuple[str, str]]:
- """Get necessary blink settings to configure dark mode for QtWebEngine."""
- if not config.val.colors.webpage.darkmode.enabled:
- return
-
- # Mapping from a colors.webpage.darkmode.algorithm setting value to
- # Chromium's DarkModeInversionAlgorithm enum values.
- algorithms = {
- # 0: kOff (not exposed)
- # 1: kSimpleInvertForTesting (not exposed)
- 'brightness-rgb': 2, # kInvertBrightness
- 'lightness-hsl': 3, # kInvertLightness
- 'lightness-cielab': 4, # kInvertLightnessLAB
- }
-
- # Mapping from a colors.webpage.darkmode.policy.images setting value to
- # Chromium's DarkModeImagePolicy enum values.
- image_policies = {
- 'always': 0, # kFilterAll
- 'never': 1, # kFilterNone
- 'smart': 2, # kFilterSmart
- }
-
- # Mapping from a colors.webpage.darkmode.policy.page setting value to
- # Chromium's DarkModePagePolicy enum values.
- page_policies = {
- 'always': 0, # kFilterAll
- 'smart': 1, # kFilterByBackground
- }
-
- bools = {
- True: 'true',
- False: 'false',
- }
-
- _setting_description_type = typing.Tuple[
- str, # qutebrowser option name
- str, # darkmode setting name
- # Mapping from the config value to a string (or something convertable
- # to a string) which gets passed to Chromium.
- typing.Optional[typing.Mapping[typing.Any, typing.Union[str, int]]],
- ]
- if qtutils.version_check('5.15', compiled=False):
- settings = [
- ('enabled', 'Enabled', bools),
- ('algorithm', 'InversionAlgorithm', algorithms),
- ] # type: typing.List[_setting_description_type]
- mandatory_setting = 'enabled'
- else:
- settings = [
- ('algorithm', '', algorithms),
- ]
- mandatory_setting = 'algorithm'
-
- settings += [
- ('contrast', 'Contrast', None),
- ('policy.images', 'ImagePolicy', image_policies),
- ('policy.page', 'PagePolicy', page_policies),
- ('threshold.text', 'TextBrightnessThreshold', None),
- ('threshold.background', 'BackgroundBrightnessThreshold', None),
- ('grayscale.all', 'Grayscale', bools),
- ('grayscale.images', 'ImageGrayscale', None),
- ]
-
- for setting, key, mapping in settings:
- # To avoid blowing up the commandline length, we only pass modified
- # settings to Chromium, as our defaults line up with Chromium's.
- # However, we always pass enabled/algorithm to make sure dark mode gets
- # actually turned on.
- value = config.instance.get(
- 'colors.webpage.darkmode.' + setting,
- fallback=setting == mandatory_setting)
- if isinstance(value, usertypes.Unset):
- continue
-
- if mapping is not None:
- value = mapping[value]
-
- # FIXME: This is "forceDarkMode" starting with Chromium 83
- prefix = 'darkMode'
-
- yield prefix + key, str(value)
-
-
-def _qtwebengine_enabled_features(
- feature_flags: typing.Sequence[str],
-) -> typing.Iterator[str]:
+def _qtwebengine_enabled_features(feature_flags: Sequence[str]) -> Iterator[str]:
"""Get --enable-features flags for QtWebEngine.
Args:
@@ -179,7 +93,7 @@ def _qtwebengine_enabled_features(
# just turn it on unconditionally on Linux, which shouldn't hurt.
yield 'WebRTCPipeWireCapturer'
- if qtutils.version_check('5.11', compiled=False) and not utils.is_mac:
+ if not utils.is_mac:
# Enable overlay scrollbars.
#
# There are two additional flags in Chromium:
@@ -195,22 +109,27 @@ def _qtwebengine_enabled_features(
if config.val.scrolling.bar == 'overlay':
yield 'OverlayScrollbar'
+ if (qtutils.version_check('5.14', compiled=False) and
+ config.val.content.headers.referer == 'same-domain'):
+ # Handling of reduced-referrer-granularity in Chromium 76+
+ # https://chromium-review.googlesource.com/c/chromium/src/+/1572699
+ #
+ # Note that this is removed entirely (and apparently the default) starting with
+ # Chromium 89 (Qt 5.15.x or 6.x):
+ # https://chromium-review.googlesource.com/c/chromium/src/+/2545444
+ yield 'ReducedReferrerGranularity'
+
def _qtwebengine_args(
namespace: argparse.Namespace,
- feature_flags: typing.Sequence[str],
-) -> typing.Iterator[str]:
+ feature_flags: Sequence[str],
+) -> Iterator[str]:
"""Get the QtWebEngine arguments to use based on the config."""
is_qt_514 = (qtutils.version_check('5.14', compiled=False) and
not qtutils.version_check('5.15', compiled=False))
- if not qtutils.version_check('5.11', compiled=False) or is_qt_514:
- # WORKAROUND equivalent to
- # https://codereview.qt-project.org/#/c/217932/
- # Needed for Qt < 5.9.5 and < 5.10.1
- #
- # For Qt 5,14, WORKAROUND for
- # https://bugreports.qt.io/browse/QTBUG-82105
+ if is_qt_514:
+ # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-82105
yield '--disable-shared-workers'
# WORKAROUND equivalent to
@@ -230,16 +149,23 @@ def _qtwebengine_args(
yield '--enable-logging'
yield '--v=1'
- blink_settings = list(_darkmode_settings())
+ if 'wait-renderer-process' in namespace.debug_flags:
+ yield '--renderer-startup-dialog'
+
+ from qutebrowser.browser.webengine import darkmode
+ blink_settings = list(darkmode.settings())
if blink_settings:
- yield '--blink-settings=' + ','.join('{}={}'.format(k, v)
- for k, v in blink_settings)
+ yield '--blink-settings=' + ','.join(f'{k}={v}' for k, v in blink_settings)
enabled_features = list(_qtwebengine_enabled_features(feature_flags))
if enabled_features:
yield '--enable-features=' + ','.join(enabled_features)
- settings = {
+ yield from _qtwebengine_settings_args()
+
+
+def _qtwebengine_settings_args() -> Iterator[str]:
+ settings: Dict[str, Dict[Any, Optional[str]]] = {
'qt.force_software_rendering': {
'software-opengl': None,
'qt-quick': None,
@@ -274,24 +200,33 @@ def _qtwebengine_args(
},
'content.headers.referer': {
'always': None,
- 'never': '--no-referrers',
- 'same-domain': '--reduced-referrer-granularity',
- }
- } # type: typing.Dict[str, typing.Dict[typing.Any, typing.Optional[str]]]
-
- if not qtutils.version_check('5.11'):
- # On Qt 5.11, we can control this via QWebEngineSettings
- settings['content.autoplay'] = {
- True: None,
- False: '--autoplay-policy=user-gesture-required',
}
+ }
- if qtutils.version_check('5.14'):
+ if (qtutils.version_check('5.14', compiled=False) and
+ not qtutils.version_check('5.15.2', compiled=False)):
+ # In Qt 5.14 to 5.15.1, `--force-dark-mode` is used to set the
+ # preferred colorscheme. In Qt 5.15.2, this is handled by a
+ # blink-setting instead.
settings['colors.webpage.prefers_color_scheme_dark'] = {
True: '--force-dark-mode',
False: None,
}
+ referrer_setting = settings['content.headers.referer']
+ if qtutils.version_check('5.14', compiled=False):
+ # Starting with Qt 5.14, this is handled via --enable-features
+ referrer_setting['same-domain'] = None
+ else:
+ referrer_setting['same-domain'] = '--reduced-referrer-granularity'
+
+ can_override_referer = (
+ qtutils.version_check('5.12.4', compiled=False) and
+ not qtutils.version_check('5.13.0', compiled=False, exact=True)
+ )
+ # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60203
+ referrer_setting['never'] = None if can_override_referer else '--no-referrers'
+
for setting, args in sorted(settings.items()):
arg = args[config.instance.get(setting)]
if arg is not None:
diff --git a/qutebrowser/config/stylesheet.py b/qutebrowser/config/stylesheet.py
index 10e6e4e52..3c68fc0b9 100644
--- a/qutebrowser/config/stylesheet.py
+++ b/qutebrowser/config/stylesheet.py
@@ -81,13 +81,13 @@ class _StyleSheetObserver(QObject):
if update:
self.setParent(self._obj)
if stylesheet is None:
- self._stylesheet = obj.STYLESHEET # type: str
+ self._stylesheet: str = obj.STYLESHEET
else:
self._stylesheet = stylesheet
if update:
- self._options = jinja.template_config_variables(
- self._stylesheet) # type: Optional[FrozenSet[str]]
+ self._options: Optional[FrozenSet[str]] = jinja.template_config_variables(
+ self._stylesheet)
else:
self._options = None
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
index cc307dd75..e1cb393dc 100644
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -20,9 +20,9 @@
"""Bridge from QWeb(Engine)Settings to our own settings."""
import re
-import typing
import argparse
import functools
+from typing import Any, Callable, Dict, Optional
import attr
from PyQt5.QtCore import QUrl, pyqtSlot, qVersion
@@ -30,7 +30,7 @@ from PyQt5.QtGui import QFont
import qutebrowser
from qutebrowser.config import config
-from qutebrowser.utils import log, usertypes, urlmatch, qtutils, utils
+from qutebrowser.utils import usertypes, urlmatch, qtutils, utils
from qutebrowser.misc import objects, debugcachestats
UNSET = object()
@@ -41,11 +41,11 @@ class UserAgent:
"""A parsed user agent."""
- os_info = attr.ib() # type: str
- webkit_version = attr.ib() # type: str
- upstream_browser_key = attr.ib() # type: str
- upstream_browser_version = attr.ib() # type: str
- qt_key = attr.ib() # type: str
+ os_info: str = attr.ib()
+ webkit_version: str = attr.ib()
+ upstream_browser_key: str = attr.ib()
+ upstream_browser_version: str = attr.ib()
+ qt_key: str = attr.ib()
@classmethod
def parse(cls, ua: str) -> 'UserAgent':
@@ -82,8 +82,7 @@ class AttributeInfo:
"""Info about a settings attribute."""
- def __init__(self, *attributes: typing.Any,
- converter: typing.Callable = None) -> None:
+ def __init__(self, *attributes: Any, converter: Callable = None) -> None:
self.attributes = attributes
if converter is None:
self.converter = lambda val: val
@@ -95,37 +94,28 @@ class AbstractSettings:
"""Abstract base class for settings set via QWeb(Engine)Settings."""
- _ATTRIBUTES = {} # type: typing.Dict[str, AttributeInfo]
- _FONT_SIZES = {} # type: typing.Dict[str, typing.Any]
- _FONT_FAMILIES = {} # type: typing.Dict[str, typing.Any]
- _FONT_TO_QFONT = {} # type: typing.Dict[typing.Any, QFont.StyleHint]
+ _ATTRIBUTES: Dict[str, AttributeInfo] = {}
+ _FONT_SIZES: Dict[str, Any] = {}
+ _FONT_FAMILIES: Dict[str, Any] = {}
+ _FONT_TO_QFONT: Dict[Any, QFont.StyleHint] = {}
- def __init__(self, settings: typing.Any) -> None:
+ def __init__(self, settings: Any) -> None:
self._settings = settings
- def _assert_not_unset(self, value: typing.Any) -> None:
+ def _assert_not_unset(self, value: Any) -> None:
assert value is not usertypes.UNSET
- def set_attribute(self, name: str, value: typing.Any) -> bool:
+ def set_attribute(self, name: str, value: Any) -> None:
"""Set the given QWebSettings/QWebEngineSettings attribute.
If the value is usertypes.UNSET, the value is reset instead.
-
- Return:
- True if there was a change, False otherwise.
"""
- old_value = self.test_attribute(name)
-
info = self._ATTRIBUTES[name]
for attribute in info.attributes:
if value is usertypes.UNSET:
self._settings.resetAttribute(attribute)
- new_value = self.test_attribute(name)
else:
self._settings.setAttribute(attribute, info.converter(value))
- new_value = value
-
- return old_value != new_value
def test_attribute(self, name: str) -> bool:
"""Get the value for the given attribute.
@@ -136,26 +126,17 @@ class AbstractSettings:
info = self._ATTRIBUTES[name]
return self._settings.testAttribute(info.attributes[0])
- def set_font_size(self, name: str, value: int) -> bool:
- """Set the given QWebSettings/QWebEngineSettings font size.
-
- Return:
- True if there was a change, False otherwise.
- """
+ def set_font_size(self, name: str, value: int) -> None:
+ """Set the given QWebSettings/QWebEngineSettings font size."""
self._assert_not_unset(value)
family = self._FONT_SIZES[name]
- old_value = self._settings.fontSize(family)
self._settings.setFontSize(family, value)
- return old_value != value
- def set_font_family(self, name: str, value: typing.Optional[str]) -> bool:
+ def set_font_family(self, name: str, value: Optional[str]) -> None:
"""Set the given QWebSettings/QWebEngineSettings font family.
With None (the default), QFont is used to get the default font for the
family.
-
- Return:
- True if there was a change, False otherwise.
"""
self._assert_not_unset(value)
family = self._FONT_FAMILIES[name]
@@ -164,23 +145,14 @@ class AbstractSettings:
font.setStyleHint(self._FONT_TO_QFONT[family])
value = font.defaultFamily()
- old_value = self._settings.fontFamily(family)
self._settings.setFontFamily(family, value)
- return value != old_value
-
- def set_default_text_encoding(self, encoding: str) -> bool:
- """Set the default text encoding to use.
-
- Return:
- True if there was a change, False otherwise.
- """
+ def set_default_text_encoding(self, encoding: str) -> None:
+ """Set the default text encoding to use."""
self._assert_not_unset(encoding)
- old_value = self._settings.defaultTextEncoding()
self._settings.setDefaultTextEncoding(encoding)
- return old_value != encoding
- def _update_setting(self, setting: str, value: typing.Any) -> bool:
+ def _update_setting(self, setting: str, value: Any) -> bool:
"""Update the given setting/value.
Unknown settings are ignored.
@@ -189,13 +161,13 @@ class AbstractSettings:
True if there was a change, False otherwise.
"""
if setting in self._ATTRIBUTES:
- return self.set_attribute(setting, value)
+ self.set_attribute(setting, value)
elif setting in self._FONT_SIZES:
- return self.set_font_size(setting, value)
+ self.set_font_size(setting, value)
elif setting in self._FONT_FAMILIES:
- return self.set_font_family(setting, value)
+ self.set_font_family(setting, value)
elif setting == 'content.default_encoding':
- return self.set_default_text_encoding(value)
+ self.set_default_text_encoding(value)
return False
def update_setting(self, setting: str) -> None:
@@ -203,27 +175,15 @@ class AbstractSettings:
value = config.instance.get(setting)
self._update_setting(setting, value)
- def update_for_url(self, url: QUrl) -> typing.Set[str]:
- """Update settings customized for the given tab.
-
- Return:
- A set of settings which actually changed.
- """
+ def update_for_url(self, url: QUrl) -> None:
+ """Update settings customized for the given tab."""
qtutils.ensure_valid(url)
- changed_settings = set()
for values in config.instance:
if not values.opt.supports_pattern:
continue
value = values.get_for_url(url, fallback=False)
-
- changed = self._update_setting(values.opt.name, value)
- if changed:
- log.config.debug("Changed for {}: {} = {}".format(
- url.toDisplayString(), values.opt.name, value))
- changed_settings.add(values.opt.name)
-
- return changed_settings
+ self._update_setting(values.opt.name, value)
def init_settings(self) -> None:
"""Set all supported settings correctly."""
@@ -268,10 +228,10 @@ def init(args: argparse.Namespace) -> None:
"""Initialize all QWeb(Engine)Settings."""
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings
- webenginesettings.init(args)
+ webenginesettings.init()
elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkitsettings
- webkitsettings.init(args)
+ webkitsettings.init()
else:
raise utils.Unreachable(objects.backend)
diff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py
index 6c5756016..fddeaabc9 100644
--- a/qutebrowser/extensions/interceptors.py
+++ b/qutebrowser/extensions/interceptors.py
@@ -19,8 +19,8 @@
"""Infrastructure for intercepting requests."""
-import typing
import enum
+from typing import Callable, List, Optional
import attr
@@ -76,15 +76,15 @@ class Request:
"""A request which can be intercepted/blocked."""
#: The URL of the page being shown.
- first_party_url = attr.ib() # type: typing.Optional[QUrl]
+ first_party_url: Optional[QUrl] = attr.ib()
#: The URL of the file being requested.
- request_url = attr.ib() # type: QUrl
+ request_url: QUrl = attr.ib()
- is_blocked = attr.ib(False) # type: bool
+ is_blocked: bool = attr.ib(False)
#: The resource type of the request. None if not supported on this backend.
- resource_type = attr.ib(None) # type: typing.Optional[ResourceType]
+ resource_type: Optional[ResourceType] = attr.ib(None)
def block(self) -> None:
"""Block this request."""
@@ -107,10 +107,10 @@ class Request:
#: Type annotation for an interceptor function.
-InterceptorType = typing.Callable[[Request], None]
+InterceptorType = Callable[[Request], None]
-_interceptors = [] # type: typing.List[InterceptorType]
+_interceptors: List[InterceptorType] = []
def register(interceptor: InterceptorType) -> None:
diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py
index 41b9c63fd..b6d86f517 100644
--- a/qutebrowser/extensions/loader.py
+++ b/qutebrowser/extensions/loader.py
@@ -19,12 +19,13 @@
"""Loader for qutebrowser extensions."""
-import importlib.abc
import pkgutil
import types
-import typing
import sys
import pathlib
+import importlib
+import argparse
+from typing import Callable, Iterator, List, Optional, Set, Tuple
import attr
@@ -35,9 +36,6 @@ from qutebrowser.config import config
from qutebrowser.utils import log, standarddir
from qutebrowser.misc import objects
-if typing.TYPE_CHECKING:
- import argparse
-
# ModuleInfo objects for all loaded plugins
_module_infos = []
@@ -48,9 +46,9 @@ class InitContext:
"""Context an extension gets in its init hook."""
- data_dir = attr.ib() # type: pathlib.Path
- config_dir = attr.ib() # type: pathlib.Path
- args = attr.ib() # type: argparse.Namespace
+ data_dir: pathlib.Path = attr.ib()
+ config_dir: pathlib.Path = attr.ib()
+ args: argparse.Namespace = attr.ib()
@attr.s
@@ -61,13 +59,11 @@ class ModuleInfo:
This gets used by qutebrowser.api.hook.
"""
- _ConfigChangedHooksType = typing.List[typing.Tuple[typing.Optional[str],
- typing.Callable]]
+ _ConfigChangedHooksType = List[Tuple[Optional[str], Callable]]
- skip_hooks = attr.ib(False) # type: bool
- init_hook = attr.ib(None) # type: typing.Optional[typing.Callable]
- config_changed_hooks = attr.ib(
- attr.Factory(list)) # type: _ConfigChangedHooksType
+ skip_hooks: bool = attr.ib(False)
+ init_hook: Optional[Callable] = attr.ib(None)
+ config_changed_hooks: _ConfigChangedHooksType = attr.ib(attr.Factory(list))
@attr.s
@@ -75,7 +71,7 @@ class ExtensionInfo:
"""Information about a qutebrowser extension."""
- name = attr.ib() # type: str
+ name: str = attr.ib()
def add_module_info(module: types.ModuleType) -> ModuleInfo:
@@ -92,7 +88,7 @@ def load_components(*, skip_hooks: bool = False) -> None:
_load_component(info, skip_hooks=skip_hooks)
-def walk_components() -> typing.Iterator[ExtensionInfo]:
+def walk_components() -> Iterator[ExtensionInfo]:
"""Yield ExtensionInfo objects for all modules."""
if hasattr(sys, 'frozen'):
yield from _walk_pyinstaller()
@@ -104,7 +100,7 @@ def _on_walk_error(name: str) -> None:
raise ImportError("Failed to import {}".format(name))
-def _walk_normal() -> typing.Iterator[ExtensionInfo]:
+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,
@@ -117,7 +113,7 @@ def _walk_normal() -> typing.Iterator[ExtensionInfo]:
yield ExtensionInfo(name=name)
-def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]:
+def _walk_pyinstaller() -> Iterator[ExtensionInfo]:
"""Walk extensions when using PyInstaller.
See https://github.com/pyinstaller/pyinstaller/issues/1905
@@ -125,7 +121,7 @@ def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]:
Inspired by:
https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py
"""
- toc = set() # type: typing.Set[str]
+ toc: Set[str] = set()
for importer in pkgutil.iter_importers('qutebrowser'):
if hasattr(importer, 'toc'):
toc |= importer.toc
diff --git a/qutebrowser/html/warning-old-qt.html b/qutebrowser/html/warning-old-qt.html
deleted file mode 100644
index e5da57548..000000000
--- a/qutebrowser/html/warning-old-qt.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "styled.html" %}
-
-{% block content %}
-<h1>{{ title }}</h1>
-<span class="note">Note this warning will only appear once. Use <span class="mono">:open
-qute://warning/old-qt</span> to show it again at a later time.</span>
-
-<p>You're using qutebrowser with Qt {{qt_version}}.</p>
-
-{% if qt_version.startswith('5.7.') %}
-<p>Qt 5.7 was released in June 2016, with the 5.7.1 patch release in December
-2016. It is based on Chromium 49 (March 2016) with (some) security fixes up to
-Chromium 54 (October 2016).</p>
-{% elif qt_version.startswith('5.8.') %}
-<p>Qt 5.8 has had various bugs, and has been unsupported (but working to some
-degree) in qutebrowser for a while.</p>
-{% elif qt_version.startswith('5.9.') %}
-<p>Qt 5.9 LTS was released in May 2017 and is based on Chromium 56 (January 2017). It is a long term support release with the 5.9.9 patch release in December 2019 including (some) security fixes up to Chromium 78 (November 2019). However, its usage was found to be low, and the next LTS (Qt 5.12) was released in December 2018.</p>
-{% elif qt_version.startswith('5.10.') %}
-<p>Qt 5.10 was released in December 2017, with the 5.10.1 patch release in February
-2018. It is based on Chromium 65 (March 2018) with (some) security fixes up to
-Chromium 70 (November 2018).</p>
-{% endif %}
-
-Also, note that QtWebEngine is <a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-information.en.html#browser-security">not covered</a> by Debian security updates.
-
-<p>Because of those security issues and the maintaince burden coming with
-supporting old versions, support for Qt < 5.11 will be dropped in qutebrowser
-v2.0. You might want to check
-<a href="https://qutebrowser.org/doc/install.html">alternate installation methods</a>
-which allow you to get a newer Qt.</p>
-{% endblock %}
diff --git a/qutebrowser/html/warning-sessions.html b/qutebrowser/html/warning-sessions.html
index fadc9908d..82bc02aab 100644
--- a/qutebrowser/html/warning-sessions.html
+++ b/qutebrowser/html/warning-sessions.html
@@ -5,11 +5,11 @@
<span class="note">Note this warning will only appear once. Use <span class="mono">:open
qute://warning/sessions</span> to show it again at a later time.</span>
-<p>You're using qutebrowser with Qt 5.15.</p>
+<p>You're using qutebrowser with Qt 5.15. While this is the recommended Qt version to use (due to QtWebEngine security updates), qutebrowser only provides partial support for session files.</p>
<p>Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.</p>
-<p>At the time of writing (April 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v1.14.0.</p>
+<p>At the time of writing (October 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v2.0.0 (planned to be released at the end of the year or early 2021).</p>
<p>As a stop-gap measure:</p>
diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml
index cbb8e17c0..939500aa3 100644
--- a/qutebrowser/javascript/.eslintrc.yaml
+++ b/qutebrowser/javascript/.eslintrc.yaml
@@ -12,9 +12,7 @@
env:
browser: true
-
-parserOptions:
- ecmaVersion: 6
+ es6: true
extends:
"eslint:all"
@@ -31,7 +29,7 @@ rules:
init-declarations: "off"
no-plusplus: "off"
no-extra-parens: "off"
- id-length: ["error", {"exceptions": ["i", "k", "x", "y"]}]
+ id-length: ["error", {"exceptions": ["i", "k", "v", "x", "y"]}]
object-shorthand: "off"
max-statements: ["error", {"max": 40}]
quotes: ["error", "double", {"avoidEscape": true}]
@@ -43,7 +41,7 @@ rules:
func-names: "off"
sort-keys: "off"
no-warning-comments: "off"
- max-len: ["error", {"ignoreUrls": true}]
+ max-len: ["error", {"ignoreUrls": true, "code": 88}]
capitalized-comments: "off"
prefer-destructuring: "off"
line-comment-position: "off"
diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js
index d7ba88fe6..0e8de5e6e 100644
--- a/qutebrowser/javascript/caret.js
+++ b/qutebrowser/javascript/caret.js
@@ -774,13 +774,6 @@ window._qutebrowser.caret = (function() {
CaretBrowsing.isWindows = null;
/**
- * Whether we're running on on old Qt 5.7.1.
- * There, we need to use -webkit-filter.
- * @type {boolean}
- */
- CaretBrowsing.needsFilterPrefix = null;
-
- /**
* The id returned by window.setInterval for our stopAnimation function, so
* we can cancel it when we call stopAnimation again.
* @type {number?}
@@ -863,7 +856,6 @@ window._qutebrowser.caret = (function() {
};
CaretBrowsing.injectCaretStyles = function() {
- const prefix = CaretBrowsing.needsFilterPrefix ? "-webkit-" : "";
const style = `
.CaretBrowsing_Caret {
position: absolute;
@@ -875,7 +867,7 @@ window._qutebrowser.caret = (function() {
background-color: var(--inherited-color, #000);
color: var(--inherited-color, #000);
mix-blend-mode: difference;
- ${prefix}filter: invert(85%);
+ filter: invert(85%);
}
@keyframes blink {
50% { visibility: hidden; }
@@ -1355,7 +1347,6 @@ window._qutebrowser.caret = (function() {
funcs.setFlags = (flags) => {
CaretBrowsing.isWindows = flags.includes("windows");
- CaretBrowsing.needsFilterPrefix = flags.includes("filter-prefix");
};
funcs.disableCaret = () => {
diff --git a/qutebrowser/javascript/object_fromentries_quirk.user.js b/qutebrowser/javascript/object_fromentries_quirk.user.js
new file mode 100644
index 000000000..6f6ad8b31
--- /dev/null
+++ b/qutebrowser/javascript/object_fromentries_quirk.user.js
@@ -0,0 +1,46 @@
+// Based on: https://gitlab.com/moongoal/js-polyfill-object.fromentries/-/tree/master
+
+/*
+ Copyright 2018 Alfredo Mungo <alfredo.mungo@protonmail.ch>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+*/
+
+"use strict";
+
+if (!Object.fromEntries) {
+ Object.defineProperty(Object, "fromEntries", {
+ value(entries) {
+ if (!entries || !entries[Symbol.iterator]) {
+ throw new Error(
+ "Object.fromEntries() requires a single iterable argument");
+ }
+
+ const obj = {};
+
+ Object.keys(entries).forEach((key) => {
+ const [k, v] = entries[key];
+ obj[k] = v;
+ });
+
+ return obj;
+ },
+ });
+}
+
diff --git a/qutebrowser/javascript/print.js b/qutebrowser/javascript/print.js
deleted file mode 100644
index ee38f1aa1..000000000
--- a/qutebrowser/javascript/print.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright 2018-2020 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 <http://www.gnu.org/licenses/>.
- */
-
-/*
- * this is a hack based on the QupZilla solution, https://github.com/QupZilla/qupzilla/commit/d3f0d766fb052dc504de2426d42f235d96b5eb60
- *
- * We go to a qute://print which triggers the print, then we cancel the request.
- */
-
-"use strict";
-
-window.print = function() {
- window.location = "qute://print";
-};
diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js
index 5cc2bd014..86b41aef4 100644
--- a/qutebrowser/javascript/webelem.js
+++ b/qutebrowser/javascript/webelem.js
@@ -67,25 +67,6 @@ window._qutebrowser.webelem = (function() {
};
}
- function get_caret_position(elem, frame) {
- // With older Chromium versions (and QtWebKit), InvalidStateError will
- // be thrown if elem doesn't have selectionStart.
- // With newer Chromium versions (>= Qt 5.10), we get null.
- try {
- return elem.selectionStart;
- } catch (err) {
- if ((err instanceof DOMException ||
- (frame && err instanceof frame.DOMException)) &&
- err.name === "InvalidStateError") {
- // nothing to do, caret_position is already null
- } else {
- // not the droid we're looking for
- throw err;
- }
- }
- return null;
- }
-
function serialize_elem(elem, frame = null) {
if (!elem) {
return null;
@@ -94,7 +75,7 @@ window._qutebrowser.webelem = (function() {
const id = elements.length;
elements[id] = elem;
- const caret_position = get_caret_position(elem, frame);
+ const caret_position = elem.selectionStart;
// isContentEditable occasionally returns undefined.
const is_content_editable = elem.isContentEditable || false;
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
index dea85aede..23b77cba1 100644
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -21,7 +21,7 @@
import string
import types
-import typing
+from typing import Mapping, MutableMapping, Optional, Sequence
import attr
from PyQt5.QtCore import pyqtSignal, QObject, Qt
@@ -37,9 +37,9 @@ class MatchResult:
"""The result of matching a keybinding."""
- match_type = attr.ib() # type: QKeySequence.SequenceMatch
- command = attr.ib() # type: typing.Optional[str]
- sequence = attr.ib() # type: keyutils.KeySequence
+ match_type: QKeySequence.SequenceMatch = attr.ib()
+ command: Optional[str] = attr.ib()
+ sequence: keyutils.KeySequence = attr.ib()
def __attrs_post_init__(self) -> None:
if self.match_type == QKeySequence.ExactMatch:
@@ -75,9 +75,8 @@ class BindingTrie:
__slots__ = 'children', 'command'
def __init__(self) -> None:
- self.children = {
- } # type: typing.MutableMapping[keyutils.KeyInfo, BindingTrie]
- self.command = None # type: typing.Optional[str]
+ self.children: MutableMapping[keyutils.KeyInfo, BindingTrie] = {}
+ self.command: Optional[str] = None
def __setitem__(self, sequence: keyutils.KeySequence,
command: str) -> None:
@@ -99,8 +98,7 @@ class BindingTrie:
def __str__(self) -> str:
return '\n'.join(self.string_lines(blank=True))
- def string_lines(self, indent: int = 0,
- blank: bool = False) -> typing.Sequence[str]:
+ def string_lines(self, indent: int = 0, blank: bool = False) -> Sequence[str]:
"""Get a list of strings for a pretty-printed version of this trie."""
lines = []
if self.command is not None:
@@ -114,7 +112,7 @@ class BindingTrie:
return lines
- def update(self, mapping: typing.Mapping) -> None:
+ def update(self, mapping: Mapping) -> None:
"""Add data from the given mapping to the trie."""
for key in mapping:
self[key] = mapping[key]
diff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py
index 6ef0dd201..d77c8702d 100644
--- a/qutebrowser/keyinput/eventfilter.py
+++ b/qutebrowser/keyinput/eventfilter.py
@@ -19,7 +19,7 @@
"""Global Qt event filter which dispatches key events."""
-import typing
+from typing import cast
from PyQt5.QtCore import pyqtSlot, QObject, QEvent
from PyQt5.QtGui import QKeyEvent, QWindow
@@ -102,7 +102,7 @@ class EventFilter(QObject):
handler = self._handlers[typ]
try:
- return handler(typing.cast(QKeyEvent, event))
+ return handler(cast(QKeyEvent, event))
except:
# If there is an exception in here and we leave the eventfilter
# activated, we'll get an infinite loop and a stack overflow.
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
index b95f4a55d..bc8c4a5fe 100644
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -32,7 +32,7 @@ handle what we actually think we do.
"""
import itertools
-import typing
+from typing import cast, overload, Iterable, Iterator, List, Mapping, Optional, Union
import attr
from PyQt5.QtCore import Qt, QEvent
@@ -53,115 +53,99 @@ _MODIFIER_MAP = {
_NIL_KEY = Qt.Key(0)
-_ModifierType = typing.Union[Qt.KeyboardModifier, Qt.KeyboardModifiers]
-
-
-def _build_special_names() -> typing.Mapping[Qt.Key, str]:
- """Build _SPECIAL_NAMES dict from the special_names_str mapping below.
-
- The reason we don't do this directly is that certain Qt versions don't have
- all the keys, so we want to ignore AttributeErrors.
- """
- special_names_str = {
- # Some keys handled in a weird way by QKeySequence::toString.
- # See https://bugreports.qt.io/browse/QTBUG-40030
- # Most are unlikely to be ever needed, but you never know ;)
- # For dead/combining keys, we return the corresponding non-combining
- # key, as that's easier to add to the config.
-
- 'Super_L': 'Super L',
- 'Super_R': 'Super R',
- 'Hyper_L': 'Hyper L',
- 'Hyper_R': 'Hyper R',
- 'Direction_L': 'Direction L',
- 'Direction_R': 'Direction R',
-
- 'Shift': 'Shift',
- 'Control': 'Control',
- 'Meta': 'Meta',
- 'Alt': 'Alt',
-
- 'AltGr': 'AltGr',
- 'Multi_key': 'Multi key',
- 'SingleCandidate': 'Single Candidate',
- 'Mode_switch': 'Mode switch',
- 'Dead_Grave': '`',
- 'Dead_Acute': '´',
- 'Dead_Circumflex': '^',
- 'Dead_Tilde': '~',
- 'Dead_Macron': '¯',
- 'Dead_Breve': '˘',
- 'Dead_Abovedot': '˙',
- 'Dead_Diaeresis': '¨',
- 'Dead_Abovering': '˚',
- 'Dead_Doubleacute': '˝',
- 'Dead_Caron': 'ˇ',
- 'Dead_Cedilla': '¸',
- 'Dead_Ogonek': '˛',
- 'Dead_Iota': 'Iota',
- 'Dead_Voiced_Sound': 'Voiced Sound',
- 'Dead_Semivoiced_Sound': 'Semivoiced Sound',
- 'Dead_Belowdot': 'Belowdot',
- 'Dead_Hook': 'Hook',
- 'Dead_Horn': 'Horn',
-
- 'Dead_Stroke': '\u0335', # '̵'
- 'Dead_Abovecomma': '\u0313', # '̓'
- 'Dead_Abovereversedcomma': '\u0314', # '̔'
- 'Dead_Doublegrave': '\u030f', # '̏'
- 'Dead_Belowring': '\u0325', # '̥'
- 'Dead_Belowmacron': '\u0331', # '̱'
- 'Dead_Belowcircumflex': '\u032d', # '̭'
- 'Dead_Belowtilde': '\u0330', # '̰'
- 'Dead_Belowbreve': '\u032e', # '̮'
- 'Dead_Belowdiaeresis': '\u0324', # '̤'
- 'Dead_Invertedbreve': '\u0311', # '̑'
- 'Dead_Belowcomma': '\u0326', # '̦'
- 'Dead_Currency': '¤',
- 'Dead_a': 'a',
- 'Dead_A': 'A',
- 'Dead_e': 'e',
- 'Dead_E': 'E',
- 'Dead_i': 'i',
- 'Dead_I': 'I',
- 'Dead_o': 'o',
- 'Dead_O': 'O',
- 'Dead_u': 'u',
- 'Dead_U': 'U',
- 'Dead_Small_Schwa': 'ə',
- 'Dead_Capital_Schwa': 'Ə',
- 'Dead_Greek': 'Greek',
- 'Dead_Lowline': '\u0332', # '̲'
- 'Dead_Aboveverticalline': '\u030d', # '̍'
- 'Dead_Belowverticalline': '\u0329',
- 'Dead_Longsolidusoverlay': '\u0338', # '̸'
-
- 'Memo': 'Memo',
- 'ToDoList': 'To Do List',
- 'Calendar': 'Calendar',
- 'ContrastAdjust': 'Contrast Adjust',
- 'LaunchG': 'Launch (G)',
- 'LaunchH': 'Launch (H)',
-
- 'MediaLast': 'Media Last',
-
- 'unknown': 'Unknown',
-
- # For some keys, we just want a different name
- 'Escape': 'Escape',
- }
- special_names = {_NIL_KEY: 'nil'}
-
- for k, v in special_names_str.items():
- try:
- special_names[getattr(Qt, 'Key_' + k)] = v
- except AttributeError: # pragma: no cover
- pass
-
- return special_names
-
-
-_SPECIAL_NAMES = _build_special_names()
+_ModifierType = Union[Qt.KeyboardModifier, Qt.KeyboardModifiers]
+
+
+_SPECIAL_NAMES = {
+ # Some keys handled in a weird way by QKeySequence::toString.
+ # See https://bugreports.qt.io/browse/QTBUG-40030
+ # Most are unlikely to be ever needed, but you never know ;)
+ # For dead/combining keys, we return the corresponding non-combining
+ # key, as that's easier to add to the config.
+
+ Qt.Key_Super_L: 'Super L',
+ Qt.Key_Super_R: 'Super R',
+ Qt.Key_Hyper_L: 'Hyper L',
+ Qt.Key_Hyper_R: 'Hyper R',
+ Qt.Key_Direction_L: 'Direction L',
+ Qt.Key_Direction_R: 'Direction R',
+
+ Qt.Key_Shift: 'Shift',
+ Qt.Key_Control: 'Control',
+ Qt.Key_Meta: 'Meta',
+ Qt.Key_Alt: 'Alt',
+
+ Qt.Key_AltGr: 'AltGr',
+ Qt.Key_Multi_key: 'Multi key',
+ Qt.Key_SingleCandidate: 'Single Candidate',
+ Qt.Key_Mode_switch: 'Mode switch',
+ Qt.Key_Dead_Grave: '`',
+ Qt.Key_Dead_Acute: '´',
+ Qt.Key_Dead_Circumflex: '^',
+ Qt.Key_Dead_Tilde: '~',
+ Qt.Key_Dead_Macron: '¯',
+ Qt.Key_Dead_Breve: '˘',
+ Qt.Key_Dead_Abovedot: '˙',
+ Qt.Key_Dead_Diaeresis: '¨',
+ Qt.Key_Dead_Abovering: '˚',
+ Qt.Key_Dead_Doubleacute: '˝',
+ Qt.Key_Dead_Caron: 'ˇ',
+ Qt.Key_Dead_Cedilla: '¸',
+ Qt.Key_Dead_Ogonek: '˛',
+ Qt.Key_Dead_Iota: 'Iota',
+ Qt.Key_Dead_Voiced_Sound: 'Voiced Sound',
+ Qt.Key_Dead_Semivoiced_Sound: 'Semivoiced Sound',
+ Qt.Key_Dead_Belowdot: 'Belowdot',
+ Qt.Key_Dead_Hook: 'Hook',
+ Qt.Key_Dead_Horn: 'Horn',
+
+ Qt.Key_Dead_Stroke: '\u0335', # '̵'
+ Qt.Key_Dead_Abovecomma: '\u0313', # '̓'
+ Qt.Key_Dead_Abovereversedcomma: '\u0314', # '̔'
+ Qt.Key_Dead_Doublegrave: '\u030f', # '̏'
+ Qt.Key_Dead_Belowring: '\u0325', # '̥'
+ Qt.Key_Dead_Belowmacron: '\u0331', # '̱'
+ Qt.Key_Dead_Belowcircumflex: '\u032d', # '̭'
+ Qt.Key_Dead_Belowtilde: '\u0330', # '̰'
+ Qt.Key_Dead_Belowbreve: '\u032e', # '̮'
+ Qt.Key_Dead_Belowdiaeresis: '\u0324', # '̤'
+ Qt.Key_Dead_Invertedbreve: '\u0311', # '̑'
+ Qt.Key_Dead_Belowcomma: '\u0326', # '̦'
+ Qt.Key_Dead_Currency: '¤',
+ Qt.Key_Dead_a: 'a',
+ Qt.Key_Dead_A: 'A',
+ Qt.Key_Dead_e: 'e',
+ Qt.Key_Dead_E: 'E',
+ Qt.Key_Dead_i: 'i',
+ Qt.Key_Dead_I: 'I',
+ Qt.Key_Dead_o: 'o',
+ Qt.Key_Dead_O: 'O',
+ Qt.Key_Dead_u: 'u',
+ Qt.Key_Dead_U: 'U',
+ Qt.Key_Dead_Small_Schwa: 'ə',
+ Qt.Key_Dead_Capital_Schwa: 'Ə',
+ Qt.Key_Dead_Greek: 'Greek',
+ Qt.Key_Dead_Lowline: '\u0332', # '̲'
+ Qt.Key_Dead_Aboveverticalline: '\u030d', # '̍'
+ Qt.Key_Dead_Belowverticalline: '\u0329',
+ Qt.Key_Dead_Longsolidusoverlay: '\u0338', # '̸'
+
+ Qt.Key_Memo: 'Memo',
+ Qt.Key_ToDoList: 'To Do List',
+ Qt.Key_Calendar: 'Calendar',
+ Qt.Key_ContrastAdjust: 'Contrast Adjust',
+ Qt.Key_LaunchG: 'Launch (G)',
+ Qt.Key_LaunchH: 'Launch (H)',
+
+ Qt.Key_MediaLast: 'Media Last',
+
+ Qt.Key_unknown: 'Unknown',
+
+ # For some keys, we just want a different name
+ Qt.Key_Escape: 'Escape',
+
+ _NIL_KEY: 'nil',
+}
def _assert_plain_key(key: Qt.Key) -> None:
@@ -231,8 +215,7 @@ def _remap_unicode(key: Qt.Key, text: str) -> Qt.Key:
return key
-def _check_valid_utf8(s: str,
- data: typing.Union[Qt.Key, _ModifierType]) -> None:
+def _check_valid_utf8(s: str, data: Union[Qt.Key, _ModifierType]) -> None:
"""Make sure the given string is valid UTF-8.
Makes sure there are no chars where Qt did fall back to weird UTF-16
@@ -288,7 +271,7 @@ class KeyParseError(Exception):
"""Raised by _parse_single_key/parse_keystring on parse errors."""
- def __init__(self, keystr: typing.Optional[str], error: str) -> None:
+ def __init__(self, keystr: Optional[str], error: str) -> None:
if keystr is None:
msg = "Could not parse keystring: {}".format(error)
else:
@@ -296,7 +279,7 @@ class KeyParseError(Exception):
super().__init__(msg)
-def _parse_keystring(keystr: str) -> typing.Iterator[str]:
+def _parse_keystring(keystr: str) -> Iterator[str]:
key = ''
special = False
for c in keystr:
@@ -353,7 +336,7 @@ def _parse_single_key(keystr: str) -> str:
return 'Shift+' + keystr if keystr.isupper() else keystr
-@attr.s(frozen=True, hash=False)
+@attr.s(frozen=True)
class KeyInfo:
"""A key with optional modifiers.
@@ -363,8 +346,8 @@ class KeyInfo:
modifiers: A Qt::KeyboardModifiers enum value.
"""
- key = attr.ib() # type: Qt.Key
- modifiers = attr.ib() # type: _ModifierType
+ key: Qt.Key = attr.ib()
+ modifiers: _ModifierType = attr.ib()
@classmethod
def from_event(cls, e: QKeyEvent) -> 'KeyInfo':
@@ -377,15 +360,7 @@ class KeyInfo:
modifiers = e.modifiers()
_assert_plain_key(key)
_assert_plain_modifier(modifiers)
- return cls(key, typing.cast(Qt.KeyboardModifier, modifiers))
-
- def __hash__(self) -> int:
- """Convert KeyInfo to int before hashing.
-
- This is needed as a WORKAROUND because enum members aren't hashable
- with PyQt 5.7.
- """
- return hash(self.to_int())
+ return cls(key, cast(Qt.KeyboardModifier, modifiers))
def __str__(self) -> str:
"""Convert this KeyInfo to a meaningful name.
@@ -473,7 +448,7 @@ class KeySequence:
_MAX_LEN = 4
def __init__(self, *keys: int) -> None:
- self._sequences = [] # type: typing.List[QKeySequence]
+ self._sequences: List[QKeySequence] = []
for sub in utils.chunk(keys, self._MAX_LEN):
args = [self._convert_key(key) for key in sub]
sequence = QKeySequence(*args)
@@ -493,7 +468,7 @@ class KeySequence:
parts.append(str(info))
return ''.join(parts)
- def __iter__(self) -> typing.Iterator[KeyInfo]:
+ def __iter__(self) -> Iterator[KeyInfo]:
"""Iterate over KeyInfo objects."""
for key_and_modifiers in self._iter_keys():
key = Qt.Key(int(key_and_modifiers) & ~Qt.KeyboardModifierMask)
@@ -535,17 +510,15 @@ class KeySequence:
def __bool__(self) -> bool:
return bool(self._sequences)
- @typing.overload
+ @overload
def __getitem__(self, item: int) -> KeyInfo:
...
- @typing.overload
+ @overload
def __getitem__(self, item: slice) -> 'KeySequence':
...
- def __getitem__(
- self, item: typing.Union[int, slice]
- ) -> typing.Union[KeyInfo, 'KeySequence']:
+ def __getitem__(self, item: Union[int, slice]) -> Union[KeyInfo, 'KeySequence']:
if isinstance(item, slice):
keys = list(self._iter_keys())
return self.__class__(*keys[item])
@@ -553,9 +526,8 @@ class KeySequence:
infos = list(self)
return infos[item]
- def _iter_keys(self) -> typing.Iterator[int]:
- sequences = typing.cast(typing.Iterable[typing.Iterable[int]],
- self._sequences)
+ def _iter_keys(self) -> Iterator[int]:
+ sequences = cast(Iterable[Iterable[int]], self._sequences)
return itertools.chain.from_iterable(sequences)
def _validate(self, keystr: str = None) -> None:
@@ -639,18 +611,6 @@ class KeySequence:
not ev.text().isupper()):
modifiers = Qt.KeyboardModifiers() # type: ignore[assignment]
- # On macOS, swap Ctrl and Meta back
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-51293
- if utils.is_mac:
- if modifiers & Qt.ControlModifier and modifiers & Qt.MetaModifier:
- pass
- elif modifiers & Qt.ControlModifier:
- modifiers &= ~Qt.ControlModifier
- modifiers |= Qt.MetaModifier
- elif modifiers & Qt.MetaModifier:
- modifiers &= ~Qt.MetaModifier
- modifiers |= Qt.ControlModifier
-
keys = list(self._iter_keys())
keys.append(key | int(modifiers))
@@ -664,7 +624,7 @@ class KeySequence:
def with_mappings(
self,
- mappings: typing.Mapping['KeySequence', 'KeySequence']
+ mappings: Mapping['KeySequence', 'KeySequence']
) -> 'KeySequence':
"""Get a new KeySequence with the given mappings applied."""
keys = []
diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py
index 6e48e5a3f..ee8883070 100644
--- a/qutebrowser/keyinput/macros.py
+++ b/qutebrowser/keyinput/macros.py
@@ -20,7 +20,7 @@
"""Keyboard macro system."""
-import typing
+from typing import cast, Dict, List, Optional, Tuple
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
@@ -28,9 +28,9 @@ from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, objreg, usertypes
-_CommandType = typing.Tuple[str, int] # command, type
+_CommandType = Tuple[str, int] # command, type
-macro_recorder = typing.cast('MacroRecorder', None)
+macro_recorder = cast('MacroRecorder', None)
class MacroRecorder:
@@ -47,10 +47,10 @@ class MacroRecorder:
"""
def __init__(self) -> None:
- self._macros = {} # type: typing.Dict[str, typing.List[_CommandType]]
- self._recording_macro = None # type: typing.Optional[str]
- self._macro_count = {} # type: typing.Dict[int, int]
- self._last_register = None # type: typing.Optional[str]
+ self._macros: Dict[str, List[_CommandType]] = {}
+ self._recording_macro: Optional[str] = None
+ self._macro_count: Dict[int, int] = {}
+ self._last_register: Optional[str] = None
@cmdutils.register(instance='macro-recorder', name='record-macro')
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index 4febf98a8..dcc6fa949 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
-"""Mode manager singleton which handles the current keyboard mode."""
+"""Mode manager (per window) which handles the current keyboard mode."""
import functools
from typing import Mapping, Callable, MutableMapping, Union, Set, cast
@@ -54,8 +54,8 @@ class KeyEvent:
text: A string (QKeyEvent::text).
"""
- key = attr.ib() # type: Qt.Key
- text = attr.ib() # type: str
+ key: Qt.Key = attr.ib()
+ text: str = attr.ib()
@classmethod
def from_event(cls, event: QKeyEvent) -> 'KeyEvent':
@@ -78,16 +78,18 @@ class UnavailableError(Exception):
def init(win_id: int, parent: QObject) -> 'ModeManager':
"""Initialize the mode manager and the keyparsers for the given win_id."""
+ commandrunner = runners.CommandRunner(win_id)
+
modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id)
- commandrunner = runners.CommandRunner(win_id)
-
hintmanager = hints.HintManager(win_id, parent=parent)
objreg.register('hintmanager', hintmanager, scope='window',
window=win_id, command_only=True)
- keyparsers = {
+ modeman.hintmanager = hintmanager
+
+ keyparsers: ParserDictType = {
usertypes.KeyMode.normal:
modeparsers.NormalKeyParser(
win_id=win_id,
@@ -184,7 +186,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
win_id=win_id,
commandrunner=commandrunner,
parent=modeman),
- } # type: ParserDictType
+ }
for mode, parser in keyparsers.items():
modeman.register(mode, parser)
@@ -227,6 +229,7 @@ class ModeManager(QObject):
Attributes:
mode: The mode we're currently in.
+ hintmanager: The HintManager associated with this window.
_win_id: The window ID of this ModeManager
_prev_mode: Mode before a prompt popped up
parsers: A dictionary of modes and their keyparsers.
@@ -256,10 +259,12 @@ class ModeManager(QObject):
def __init__(self, win_id: int, parent: QObject = None) -> None:
super().__init__(parent)
self._win_id = win_id
- self.parsers = {} # type: ParserDictType
+ self.parsers: ParserDictType = {}
self._prev_mode = usertypes.KeyMode.normal
self.mode = usertypes.KeyMode.normal
- self._releaseevents_to_pass = set() # type: Set[KeyEvent]
+ self._releaseevents_to_pass: Set[KeyEvent] = set()
+ # Set after __init__
+ self.hintmanager = cast(hints.HintManager, None)
def __repr__(self) -> str:
return utils.get_repr(self, mode=self.mode)
@@ -452,12 +457,12 @@ class ModeManager(QObject):
Return:
True if event should be filtered, False otherwise.
"""
- handlers = {
+ handlers: Mapping[QEvent.Type, Callable[[QKeyEvent], bool]] = {
QEvent.KeyPress: self._handle_keypress,
QEvent.KeyRelease: self._handle_keyrelease,
QEvent.ShortcutOverride:
functools.partial(self._handle_keypress, dry_run=True),
- } # type: Mapping[QEvent.Type, Callable[[QKeyEvent], bool]]
+ }
handler = handlers[event.type()]
return handler(cast(QKeyEvent, event))
diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py
index a55639898..48f3594a5 100644
--- a/qutebrowser/keyinput/modeparsers.py
+++ b/qutebrowser/keyinput/modeparsers.py
@@ -23,9 +23,9 @@ Module attributes:
STARTCHARS: Possible chars for starting a commandline input.
"""
-import typing
import traceback
import enum
+from typing import TYPE_CHECKING, Sequence
from PyQt5.QtCore import pyqtSlot, Qt, QObject
from PyQt5.QtGui import QKeySequence, QKeyEvent
@@ -35,12 +35,20 @@ from qutebrowser.commands import cmdexc
from qutebrowser.config import config
from qutebrowser.keyinput import basekeyparser, keyutils, macros
from qutebrowser.utils import usertypes, log, message, objreg, utils
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.commands import runners
STARTCHARS = ":/?"
-LastPress = enum.Enum('LastPress', ['none', 'filtertext', 'keystring'])
+
+
+class LastPress(enum.Enum):
+
+ """Whether the last keypress filtered a text or was part of a keystring."""
+
+ none = enum.auto()
+ filtertext = enum.auto()
+ keystring = enum.auto()
class CommandKeyParser(basekeyparser.BaseKeyParser):
@@ -224,7 +232,7 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
return match
- def update_bindings(self, strings: typing.Sequence[str],
+ def update_bindings(self, strings: Sequence[str],
preserve_filter: bool = False) -> None:
"""Update bindings when the hint strings changed.
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index faccdc73c..6273b3382 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -23,9 +23,9 @@ import binascii
import base64
import itertools
import functools
-import typing
+from typing import List, MutableSequence, Optional, Tuple, cast
-from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QRect, QPoint, QTimer, Qt,
+from PyQt5.QtCore import (pyqtBoundSignal, pyqtSlot, QRect, QPoint, QTimer, Qt,
QCoreApplication, QEventLoop, QByteArray)
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
from PyQt5.QtGui import QPalette
@@ -105,23 +105,20 @@ def raise_window(window, alert=True):
def get_target_window():
"""Get the target window for new tabs, or None if none exist."""
+ getters = {
+ 'last-focused': objreg.last_focused_window,
+ 'first-opened': objreg.first_opened_window,
+ 'last-opened': objreg.last_opened_window,
+ 'last-visible': objreg.last_visible_window,
+ }
+ getter = getters[config.val.new_instance_open_target_window]
try:
- win_mode = config.val.new_instance_open_target_window
- if win_mode == 'last-focused':
- return objreg.last_focused_window()
- elif win_mode == 'first-opened':
- return objreg.window_by_index(0)
- elif win_mode == 'last-opened':
- return objreg.window_by_index(-1)
- elif win_mode == 'last-visible':
- return objreg.last_visible_window()
- else:
- raise ValueError("Invalid win_mode {}".format(win_mode))
+ return getter()
except objreg.NoWindow:
return None
-_OverlayInfoType = typing.Tuple[QWidget, pyqtSignal, bool, str]
+_OverlayInfoType = Tuple[QWidget, pyqtBoundSignal, bool, str]
class MainWindow(QWidget):
@@ -190,8 +187,8 @@ class MainWindow(QWidget):
def __init__(self, *,
private: bool,
- geometry: typing.Optional[QByteArray] = None,
- parent: typing.Optional[QWidget] = None) -> None:
+ geometry: Optional[QByteArray] = None,
+ parent: Optional[QWidget] = None) -> None:
"""Create a new main window.
Args:
@@ -206,9 +203,11 @@ class MainWindow(QWidget):
from qutebrowser.mainwindow.statusbar import bar
self.setAttribute(Qt.WA_DeleteOnClose)
- self.setAttribute(Qt.WA_TranslucentBackground)
- self.palette().setColor(QPalette.Window, Qt.transparent)
- self._overlays = [] # type: typing.MutableSequence[_OverlayInfoType]
+ if config.val.window.transparent:
+ self.setAttribute(Qt.WA_TranslucentBackground)
+ self.palette().setColor(QPalette.Window, Qt.transparent)
+
+ self._overlays: MutableSequence[_OverlayInfoType] = []
self.win_id = next(win_id_gen)
self.registry = objreg.ObjectRegistry()
objreg.window_registry[self.win_id] = self
@@ -218,10 +217,6 @@ class MainWindow(QWidget):
objreg.register('tab-registry', tab_registry, scope='window',
window=self.win_id)
- message_bridge = message.MessageBridge(self)
- objreg.register('message-bridge', message_bridge, scope='window',
- window=self.win_id)
-
self.setWindowTitle('qutebrowser')
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
@@ -233,9 +228,8 @@ class MainWindow(QWidget):
self.is_private = config.val.content.private_browsing or private
- self.tabbed_browser = tabbedbrowser.TabbedBrowser(
- win_id=self.win_id, private=self.is_private, parent=self
- ) # type: tabbedbrowser.TabbedBrowser
+ self.tabbed_browser: tabbedbrowser.TabbedBrowser = tabbedbrowser.TabbedBrowser(
+ win_id=self.win_id, private=self.is_private, parent=self)
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
window=self.win_id)
self._init_command_dispatcher()
@@ -420,7 +414,7 @@ class MainWindow(QWidget):
self._vbox.removeWidget(self.tabbed_browser.widget)
self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status)
- widgets = [self.tabbed_browser.widget] # type: typing.List[QWidget]
+ widgets: List[QWidget] = [self.tabbed_browser.widget]
downloads_position = config.val.downloads.position
if downloads_position == 'top':
@@ -484,13 +478,8 @@ class MainWindow(QWidget):
"""Set some sensible default geometry."""
self.setGeometry(QRect(50, 50, 800, 600))
- def _get_object(self, name):
- """Get an object for this window in the object registry."""
- return objreg.get(name, scope='window', window=self.win_id)
-
def _connect_signals(self):
"""Connect all mainwindow signals."""
- message_bridge = self._get_object('message-bridge')
mode_manager = modeman.instance(self.win_id)
# misc
@@ -498,23 +487,19 @@ class MainWindow(QWidget):
mode_manager.entered.connect(hints.on_mode_entered)
# status bar
+ mode_manager.hintmanager.set_text.connect(self.status.set_text)
mode_manager.entered.connect(self.status.on_mode_entered)
mode_manager.left.connect(self.status.on_mode_left)
mode_manager.left.connect(self.status.cmd.on_mode_left)
- mode_manager.left.connect(
- message.global_bridge.mode_left) # type: ignore[arg-type]
+ mode_manager.left.connect(message.global_bridge.mode_left)
# commands
mode_manager.keystring_updated.connect(
self.status.keystring.on_keystring_updated)
- self.status.cmd.got_cmd[str].connect( # type: ignore[index]
- self._commandrunner.run_safely)
- self.status.cmd.got_cmd[str, int].connect( # type: ignore[index]
- self._commandrunner.run_safely)
- self.status.cmd.returnPressed.connect(
- self.tabbed_browser.on_cmd_return_pressed)
- self.status.cmd.got_search.connect(
- self._command_dispatcher.search)
+ self.status.cmd.got_cmd[str].connect(self._commandrunner.run_safely)
+ self.status.cmd.got_cmd[str, int].connect(self._commandrunner.run_safely)
+ self.status.cmd.returnPressed.connect(self.tabbed_browser.on_cmd_return_pressed)
+ self.status.cmd.got_search.connect(self._command_dispatcher.search)
# key hint popup
mode_manager.keystring_updated.connect(self._keyhint.update_keyhint)
@@ -526,10 +511,6 @@ class MainWindow(QWidget):
message.global_bridge.clear_messages.connect(
self._messageview.clear_messages)
- message_bridge.s_set_text.connect(self.status.set_text)
- message_bridge.s_maybe_reset_text.connect(
- self.status.txt.maybe_reset_text)
-
# statusbar
self.tabbed_browser.current_tab_changed.connect(
self.status.on_tab_changed)
@@ -578,11 +559,11 @@ class MainWindow(QWidget):
def _set_decoration(self, hidden):
"""Set the visibility of the window decoration via Qt."""
- window_flags = Qt.Window # type: int
+ window_flags: int = Qt.Window
refresh_window = self.isVisible()
if hidden:
window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint
- self.setWindowFlags(typing.cast(Qt.WindowFlags, window_flags))
+ self.setWindowFlags(cast(Qt.WindowFlags, window_flags))
if refresh_window:
self.show()
diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py
index 1f6295d89..9c4b63084 100644
--- a/qutebrowser/mainwindow/messageview.py
+++ b/qutebrowser/mainwindow/messageview.py
@@ -19,7 +19,7 @@
"""Showing messages above the statusbar."""
-import typing
+from typing import MutableSequence
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
@@ -76,7 +76,7 @@ class MessageView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
- self._messages = [] # type: typing.MutableSequence[Message]
+ self._messages: MutableSequence[Message] = []
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index a929d6428..42b6c3d97 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -23,7 +23,7 @@ import os.path
import html
import collections
import functools
-import typing
+from typing import Deque, MutableSequence, Optional, cast
import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
@@ -40,7 +40,7 @@ from qutebrowser.api import cmdutils
from qutebrowser.utils import urlmatch
-prompt_queue = typing.cast('PromptQueue', None)
+prompt_queue = cast('PromptQueue', None)
@attr.s
@@ -102,9 +102,8 @@ class PromptQueue(QObject):
super().__init__(parent)
self._question = None
self._shutting_down = False
- self._loops = [] # type: typing.MutableSequence[qtutils.EventLoop]
- self._queue = collections.deque(
- ) # type: typing.Deque[usertypes.Question]
+ self._loops: MutableSequence[qtutils.EventLoop] = []
+ self._queue: Deque[usertypes.Question] = collections.deque()
message.global_bridge.mode_left.connect(self._on_mode_left)
def __repr__(self):
@@ -196,8 +195,8 @@ class PromptQueue(QObject):
question.completed.connect(loop.quit)
question.completed.connect(loop.deleteLater)
log.prompt.debug("Starting loop.exec_() for {}".format(question))
- flags = typing.cast(QEventLoop.ProcessEventsFlags,
- QEventLoop.ExcludeSocketNotifiers)
+ flags = cast(QEventLoop.ProcessEventsFlags,
+ QEventLoop.ExcludeSocketNotifiers)
loop.exec_(flags)
log.prompt.debug("Ending loop.exec_() for {}".format(question))
@@ -289,7 +288,7 @@ class PromptContainer(QWidget):
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(10, 10, 10, 10)
self._win_id = win_id
- self._prompt = None # type: typing.Optional[_BasePrompt]
+ self._prompt: Optional[_BasePrompt] = None
self.setObjectName('PromptContainer')
self.setAttribute(Qt.WA_StyledBackground, True)
@@ -591,6 +590,7 @@ class LineEditPrompt(_BasePrompt):
self._vbox.addWidget(self._lineedit)
if question.default:
self._lineedit.setText(question.default)
+ self._lineedit.selectAll()
self.setFocusProxy(self._lineedit)
self._init_key_label()
@@ -794,8 +794,7 @@ class DownloadFilenamePrompt(FilenamePrompt):
def download_open(self, cmdline, pdfjs):
if pdfjs:
- target = downloads.PDFJSDownloadTarget(
- ) # type: downloads._DownloadTarget
+ target: 'downloads._DownloadTarget' = downloads.PDFJSDownloadTarget()
else:
target = downloads.OpenFileDownloadTarget(cmdline)
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index f83c77db9..821ea030b 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -31,8 +31,7 @@ from qutebrowser.keyinput import modeman
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
keystring, percentage, url,
- tabindex)
-from qutebrowser.mainwindow.statusbar import text as textwidget
+ tabindex, textbase)
@attr.s
@@ -49,7 +48,14 @@ class ColorFlags:
passthrough: If we're currently in passthrough-mode.
"""
- CaretMode = enum.Enum('CaretMode', ['off', 'on', 'selection'])
+ class CaretMode(enum.Enum):
+
+ """The current caret "sub-mode" we're in."""
+
+ off = enum.auto()
+ on = enum.auto()
+ selection = enum.auto()
+
prompt = attr.ib(False)
insert = attr.ib(False)
command = attr.ib(False)
@@ -180,7 +186,7 @@ class StatusBar(QWidget):
objreg.register('status-command', self.cmd, scope='window',
window=win_id)
- self.txt = textwidget.Text()
+ self.txt = textbase.TextBase()
self._stack.addWidget(self.txt)
self.cmd.show_cmd.connect(self._show_cmd_widget)
@@ -328,7 +334,7 @@ class StatusBar(QWidget):
else:
suffix = ''
text = "-- {} MODE --{}".format(mode.upper(), suffix)
- self.txt.set_text(self.txt.Text.normal, text)
+ self.txt.setText(text)
def _show_cmd_widget(self):
"""Show command widget instead of temporary text."""
@@ -342,9 +348,10 @@ class StatusBar(QWidget):
self.maybe_hide()
@pyqtSlot(str)
- def set_text(self, val):
+ def set_text(self, text):
"""Set a normal (persistent) text in the status bar."""
- self.txt.set_text(self.txt.Text.normal, val)
+ log.message.debug(text)
+ self.txt.setText(text)
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
@@ -372,7 +379,7 @@ class StatusBar(QWidget):
if mode_manager.parsers[new_mode].passthrough:
self._set_mode_text(new_mode.name)
else:
- self.txt.set_text(self.txt.Text.normal, '')
+ self.txt.setText('')
if old_mode in [usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret,
diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py
index ebd9d3921..da48d1fbd 100644
--- a/qutebrowser/mainwindow/statusbar/command.py
+++ b/qutebrowser/mainwindow/statusbar/command.py
@@ -71,10 +71,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.history.changed.connect(command_history.changed)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
- self.cursorPositionChanged.connect(
- self.update_completion) # type: ignore[arg-type]
- self.textChanged.connect(
- self.update_completion) # type: ignore[arg-type]
+ self.cursorPositionChanged.connect(self.update_completion)
+ self.textChanged.connect(self.update_completion)
self.textChanged.connect(self.updateGeometry)
self.textChanged.connect(self._incremental_search)
@@ -149,7 +147,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
raise cmdutils.CommandError(
"Invalid command text '{}'.".format(text))
if run_on_count and count is not None:
- self.got_cmd[str, int].emit(text, count) # type: ignore[index]
+ self.got_cmd[str, int].emit(text, count)
else:
self.set_cmd_text(text)
@@ -199,7 +197,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
'cmd accept')
if not was_search:
- self.got_cmd[str].emit(text[1:]) # type: ignore[index]
+ self.got_cmd[str].emit(text[1:])
@cmdutils.register(instance='status-command', scope='window')
def edit_command(self, run: bool = False) -> None:
diff --git a/qutebrowser/mainwindow/statusbar/text.py b/qutebrowser/mainwindow/statusbar/text.py
deleted file mode 100644
index 449836740..000000000
--- a/qutebrowser/mainwindow/statusbar/text.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2014-2020 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 <http://www.gnu.org/licenses/>.
-
-"""Text displayed in the statusbar."""
-
-import enum
-
-from PyQt5.QtCore import pyqtSlot
-
-from qutebrowser.mainwindow.statusbar import textbase
-from qutebrowser.utils import log
-
-
-class Text(textbase.TextBase):
-
- """Text displayed in the statusbar.
-
- Attributes:
- _normaltext: The "permanent" text. Never automatically cleared.
- _temptext: The temporary text to display.
-
- The temptext is shown from StatusBar when a temporary text or error is
- available. If not, the permanent text is shown.
- """
-
- Text = enum.Enum('Text', ['normal', 'temp'])
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self._normaltext = ''
- self._temptext = ''
-
- def set_text(self, which, text):
- """Set a text.
-
- Args:
- which: Which text to set, a self.Text instance.
- text: The text to set.
- """
- log.statusbar.debug("Setting {} text to '{}'.".format(
- which.name, text))
- if which is self.Text.normal:
- self._normaltext = text
- elif which is self.Text.temp:
- self._temptext = text
- else:
- raise ValueError("Invalid value {} for which!".format(which))
- self.update_text()
-
- @pyqtSlot(str)
- def maybe_reset_text(self, text):
- """Clear a normal text if it still matches an expected text."""
- if self._normaltext == text:
- log.statusbar.debug("Resetting: '{}'".format(text))
- self.set_text(self.Text.normal, '')
- else:
- log.statusbar.debug("Ignoring reset: '{}'".format(text))
-
- def update_text(self):
- """Update QLabel text when needed."""
- if self._temptext:
- self.setText(self._temptext)
- elif self._normaltext:
- self.setText(self._normaltext)
- else:
- self.setText('')
diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py
index c8300dc97..db8905345 100644
--- a/qutebrowser/mainwindow/statusbar/url.py
+++ b/qutebrowser/mainwindow/statusbar/url.py
@@ -29,9 +29,19 @@ from qutebrowser.config import stylesheet
from qutebrowser.utils import usertypes, urlutils
-# Note this has entries for success/error/warn from widgets.webview:LoadStatus
-UrlType = enum.Enum('UrlType', ['success', 'success_https', 'error', 'warn',
- 'hover', 'normal'])
+class UrlType(enum.Enum):
+
+ """The type/color of the URL being shown.
+
+ Note this has entries for success/error/warn from widgets.webview:LoadStatus.
+ """
+
+ success = enum.auto()
+ success_https = enum.auto()
+ error = enum.auto()
+ warn = enum.auto()
+ hover = enum.auto()
+ normal = enum.auto()
class UrlText(textbase.TextBase):
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 0f9cafac0..c67e5fa0e 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -22,13 +22,13 @@
import collections
import functools
import weakref
-import typing
import datetime
+from typing import (
+ Any, Deque, List, Mapping, MutableMapping, MutableSequence, Optional, Tuple)
import attr
from PyQt5.QtWidgets import QSizePolicy, QWidget, QApplication
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
-from PyQt5.QtGui import QIcon
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
@@ -69,12 +69,10 @@ class TabDeque:
size = config.val.tabs.focus_stack_size
if size < 0:
size = None
- self._stack = collections.deque(
- maxlen=size
- ) # type: typing.Deque[weakref.ReferenceType[QWidget]]
+ self._stack: Deque[weakref.ReferenceType[QWidget]] = collections.deque(
+ maxlen=size)
# Items that have been removed from the primary stack.
- self._stack_deleted = [
- ] # type: typing.List[weakref.ReferenceType[QWidget]]
+ self._stack_deleted: List[weakref.ReferenceType[QWidget]] = []
self._ignore_next = False
self._keep_deleted_next = False
@@ -95,7 +93,7 @@ class TabDeque:
Throws IndexError on failure.
"""
- tab = None # type: typing.Optional[QWidget]
+ tab: Optional[QWidget] = None
while tab is None or tab.pending_removal or tab is cur_tab:
tab = self._stack.pop()()
self._stack_deleted.append(weakref.ref(cur_tab))
@@ -107,7 +105,7 @@ class TabDeque:
Throws IndexError on failure.
"""
- tab = None # type: typing.Optional[QWidget]
+ tab: Optional[QWidget] = None
while tab is None or tab.pending_removal or tab is cur_tab:
tab = self._stack_deleted.pop()()
# On next tab-switch, current tab will be added to stack as normal.
@@ -212,32 +210,26 @@ class TabbedBrowser(QWidget):
self._tab_insert_idx_right = -1
self.is_shutting_down = False
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
- self.widget.new_tab_requested.connect(
- self.tabopen) # type: ignore[arg-type]
+ self.widget.new_tab_requested.connect(self.tabopen)
self.widget.currentChanged.connect(self._on_current_changed)
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
- if qtutils.version_check('5.10', compiled=False):
- self.cur_load_finished.connect(self._leave_modes_on_load)
- else:
- self.cur_load_started.connect(self._leave_modes_on_load)
+ # load_finished instead of load_started as WORKAROUND for
+ # https://bugreports.qt.io/browse/QTBUG-65223
+ self.cur_load_finished.connect(self._leave_modes_on_load)
# This init is never used, it is immediately thrown away in the next
# line.
- self.undo_stack = (
- collections.deque()
- ) # type: typing.MutableSequence[typing.MutableSequence[_UndoEntry]]
+ self.undo_stack: MutableSequence[MutableSequence[_UndoEntry]] = (
+ collections.deque())
self._update_stack_size()
self._filter = signalfilter.SignalFilter(win_id, self)
self._now_focused = None
self.search_text = None
- self.search_options = {} # type: typing.Mapping[str, typing.Any]
- self._local_marks = {
- } # type: typing.MutableMapping[QUrl, typing.MutableMapping[str, int]]
- self._global_marks = {
- } # type: typing.MutableMapping[str, typing.Tuple[int, QUrl]]
+ self.search_options: Mapping[str, Any] = {}
+ self._local_marks: MutableMapping[QUrl, MutableMapping[str, int]] = {}
+ self._global_marks: MutableMapping[str, Tuple[int, QUrl]] = {}
self.default_window_icon = self.widget.window().windowIcon()
self.is_private = private
self.tab_deque = TabDeque()
@@ -351,6 +343,8 @@ class TabbedBrowser(QWidget):
functools.partial(self._on_title_changed, tab))
tab.icon_changed.connect(
functools.partial(self._on_icon_changed, tab))
+ tab.pinned_changed.connect(
+ functools.partial(self._on_pinned_changed, tab))
tab.load_progress.connect(
functools.partial(self._on_load_progress, tab))
tab.load_finished.connect(
@@ -485,16 +479,7 @@ class TabbedBrowser(QWidget):
tab.private_api.shutdown()
self.widget.removeTab(idx)
- if not crashed:
- # WORKAROUND for a segfault when we delete the crashed tab.
- # see https://bugreports.qt.io/browse/QTBUG-58698
-
- if not qtutils.version_check('5.12'):
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58982
- # Seems to affect Qt 5.7-5.11 as well.
- tab.layout().unwrap()
-
- tab.deleteLater()
+ tab.deleteLater()
def undo(self, depth=1):
"""Undo removing of a tab or tabs."""
@@ -530,7 +515,7 @@ class TabbedBrowser(QWidget):
newtab = self.tabopen(background=False, idx=entry.index)
newtab.history.private_api.deserialize(entry.history)
- self.widget.set_tab_pinned(newtab, entry.pinned)
+ newtab.set_pinned(entry.pinned)
@pyqtSlot('QUrl', bool)
def load_url(self, url, newtab):
@@ -637,9 +622,6 @@ class TabbedBrowser(QWidget):
self.widget.currentWidget().setFocus()
else:
self.widget.setCurrentWidget(tab)
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
- # Still seems to be needed with Qt 5.11.1
- tab.setFocus()
mode = modeman.instance(self._win_id).mode
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
@@ -788,26 +770,21 @@ class TabbedBrowser(QWidget):
if not self.widget.page_title(idx):
self.widget.set_page_title(idx, url.toDisplayString())
- @pyqtSlot(browsertab.AbstractTab, QIcon)
- def _on_icon_changed(self, tab, icon):
+ @pyqtSlot(browsertab.AbstractTab)
+ def _on_icon_changed(self, tab):
"""Set the icon of a tab.
Slot for the iconChanged signal of any tab.
Args:
tab: The WebView where the title was changed.
- icon: The new icon
"""
- if not tab.data.should_show_icon():
- return
try:
- idx = self._tab_index(tab)
+ self._tab_index(tab)
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
- self.widget.setTabIcon(idx, icon)
- if config.val.tabs.tabs_are_windows:
- self.widget.window().setWindowIcon(icon)
+ self.widget.update_tab_favicon(tab)
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
@@ -886,7 +863,7 @@ class TabbedBrowser(QWidget):
start = config.cache['colors.tabs.indicator.start']
stop = config.cache['colors.tabs.indicator.stop']
system = config.cache['colors.tabs.indicator.system']
- color = utils.interpolate_color(start, stop, perc, system)
+ color = qtutils.interpolate_color(start, stop, perc, system)
self.widget.set_tab_indicator_color(idx, color)
self.widget.update_tab_title(idx)
if idx == self.widget.currentIndex():
@@ -903,7 +880,7 @@ class TabbedBrowser(QWidget):
start = config.cache['colors.tabs.indicator.start']
stop = config.cache['colors.tabs.indicator.stop']
system = config.cache['colors.tabs.indicator.system']
- color = utils.interpolate_color(start, stop, 100, system)
+ color = qtutils.interpolate_color(start, stop, 100, system)
else:
color = config.cache['colors.tabs.indicator.error']
self.widget.set_tab_indicator_color(idx, color)
@@ -922,6 +899,12 @@ class TabbedBrowser(QWidget):
self._update_window_title('scroll_pos')
self.widget.update_tab_title(idx, 'scroll_pos')
+ def _on_pinned_changed(self, tab):
+ """Update the tab's pinned status."""
+ idx = self.widget.indexOf(tab)
+ self.widget.update_tab_favicon(tab)
+ self.widget.update_tab_title(idx)
+
def _on_audio_changed(self, tab, _muted):
"""Update audio field in tab when mute or recentlyAudible changed."""
try:
@@ -954,18 +937,11 @@ class TabbedBrowser(QWidget):
tab.set_html(html)
log.webview.error(msg)
- if qtutils.version_check('5.9', compiled=False):
- url_string = tab.url(requested=True).toDisplayString()
- error_page = jinja.render(
- 'error.html', title="Error loading {}".format(url_string),
- url=url_string, error=msg)
- QTimer.singleShot(100, lambda: show_error_page(error_page))
- else:
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698
- message.error(msg)
- self._remove_tab(tab, crashed=True)
- if self.widget.count() == 0:
- self.tabopen(QUrl('about:blank'))
+ url_string = tab.url(requested=True).toDisplayString()
+ error_page = jinja.render(
+ 'error.html', title="Error loading {}".format(url_string),
+ url=url_string, error=msg)
+ QTimer.singleShot(100, lambda: show_error_page(error_page))
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index 3f94f9901..f853f8fd9 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -19,9 +19,9 @@
"""The tab widget used for TabbedBrowser from browser.py."""
-import typing
import functools
import contextlib
+from typing import Optional, cast
import attr
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,
@@ -60,8 +60,7 @@ class TabWidget(QTabWidget):
bar = TabBar(win_id, self)
self.setStyle(TabBarStyle())
self.setTabBar(bar)
- bar.tabCloseRequested.connect(
- self.tabCloseRequested) # type: ignore[arg-type]
+ bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self.update_tab_titles))
bar.currentChanged.connect(self._on_current_changed)
@@ -99,19 +98,6 @@ class TabWidget(QTabWidget):
bar.set_tab_data(idx, 'indicator-color', color)
bar.update(bar.tabRect(idx))
- def set_tab_pinned(self, tab: QWidget,
- pinned: bool) -> None:
- """Set the tab status as pinned.
-
- Args:
- tab: The tab to pin
- pinned: Pinned tab state to set.
- """
- idx = self.indexOf(tab)
- tab.data.pinned = pinned
- self.update_tab_favicon(tab)
- self.update_tab_title(idx)
-
def tab_indicator_color(self, idx):
"""Get the tab indicator color for the given index."""
return self.tabBar().tab_indicator_color(idx)
@@ -139,6 +125,7 @@ class TabWidget(QTabWidget):
field: A field name which was updated. If given, the title
is only set if the given field is in the template.
"""
+ assert idx != -1
tab = self.widget(idx)
if tab.data.pinned:
fmt = config.cache['tabs.title.format_pinned']
@@ -344,19 +331,15 @@ class TabWidget(QTabWidget):
"""Update favicon of the given tab."""
idx = self.indexOf(tab)
- if tab.data.should_show_icon():
- self.setTabIcon(idx, tab.icon())
- if config.val.tabs.tabs_are_windows:
- self.window().setWindowIcon(tab.icon())
- else:
- self.setTabIcon(idx, QIcon())
- if config.val.tabs.tabs_are_windows:
- self.window().setWindowIcon(self.window().windowIcon())
+ icon = tab.icon() if tab.data.should_show_icon() else QIcon()
+ self.setTabIcon(idx, icon)
+
+ if config.val.tabs.tabs_are_windows:
+ self.window().setWindowIcon(tab.icon())
def setTabIcon(self, idx: int, icon: QIcon) -> None:
"""Always show tab icons for pinned tabs in some circumstances."""
- tab = typing.cast(typing.Optional[browsertab.AbstractTab],
- self.widget(idx))
+ tab = cast(Optional[browsertab.AbstractTab], self.widget(idx))
if (icon.isNull() and
config.cache['tabs.favicons.show'] != 'never' and
config.cache['tabs.pinned.shrink'] and
@@ -656,7 +639,7 @@ class TabBar(QTabBar):
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
perc = int(confwidth.rstrip('%'))
- width = main_window.width() * perc / 100
+ width = main_window.width() * perc // 100
else:
width = int(confwidth)
size = QSize(width, height)
diff --git a/qutebrowser/mainwindow/windowundo.py b/qutebrowser/mainwindow/windowundo.py
index 4a4ea5d66..af7b2766a 100644
--- a/qutebrowser/mainwindow/windowundo.py
+++ b/qutebrowser/mainwindow/windowundo.py
@@ -20,7 +20,7 @@
"""Code for :undo --window."""
import collections
-import typing
+from typing import MutableSequence, cast
import attr
from PyQt5.QtCore import QObject
@@ -30,7 +30,7 @@ from qutebrowser.config import config
from qutebrowser.mainwindow import mainwindow
-instance = typing.cast('WindowUndoManager', None)
+instance = cast('WindowUndoManager', None)
@attr.s
@@ -48,9 +48,7 @@ class WindowUndoManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
- self._undos = (
- collections.deque()
- ) # type: typing.MutableSequence[_WindowUndoEntry]
+ self._undos: MutableSequence[_WindowUndoEntry] = collections.deque()
QApplication.instance().window_closing.connect(self._on_window_closing)
config.instance.changed.connect(self._on_config_changed)
diff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py
index 4838d55ed..49d648f58 100644
--- a/qutebrowser/misc/autoupdate.py
+++ b/qutebrowser/misc/autoupdate.py
@@ -55,7 +55,7 @@ class PyPIVersionClient(QObject):
self._client = httpclient.HTTPClient(self)
else:
self._client = client
- self._client.error.connect(self.error) # type: ignore[arg-type]
+ self._client.error.connect(self.error)
self._client.success.connect(self.on_client_success)
def get_version(self, package='qutebrowser'):
diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py
index 089e3191f..e459d81c1 100644
--- a/qutebrowser/misc/backendproblem.py
+++ b/qutebrowser/misc/backendproblem.py
@@ -25,8 +25,8 @@ import functools
import html
import enum
import shutil
-import typing
import argparse
+from typing import Any, List, Sequence, Tuple
import attr
from PyQt5.QtCore import Qt
@@ -55,15 +55,13 @@ class _Button:
"""A button passed to BackendProblemDialog."""
- text = attr.ib() # type: str
- setting = attr.ib() # type: str
- value = attr.ib() # type: typing.Any
- default = attr.ib(default=False) # type: bool
+ text: str = attr.ib()
+ setting: str = attr.ib()
+ value: Any = attr.ib()
+ default: bool = attr.ib(default=False)
-def _other_backend(
- backend: usertypes.Backend
-) -> typing.Tuple[usertypes.Backend, str]:
+def _other_backend(backend: usertypes.Backend) -> Tuple[usertypes.Backend, str]:
"""Get the other backend enum/setting for a given backend."""
other_backend = {
usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine,
@@ -103,7 +101,7 @@ class _Dialog(QDialog):
def __init__(self, *, because: str,
text: str,
backend: usertypes.Backend,
- buttons: typing.Sequence[_Button] = None,
+ buttons: Sequence[_Button] = None,
parent: QWidget = None) -> None:
super().__init__(parent)
vbox = QVBoxLayout(self)
@@ -157,31 +155,23 @@ class _BackendImports:
"""Whether backend modules could be imported."""
- webkit_available = attr.ib(default=None) # type: bool
- webengine_available = attr.ib(default=None) # type: bool
- webkit_error = attr.ib(default=None) # type: str
- webengine_error = attr.ib(default=None) # type: str
+ webkit_available: bool = attr.ib(default=None)
+ webengine_available: bool = attr.ib(default=None)
+ webkit_error: str = attr.ib(default=None)
+ webengine_error: str = attr.ib(default=None)
class _BackendProblemChecker:
"""Check for various backend-specific issues."""
- SOFTWARE_RENDERING_TEXT = (
- "<p><b>Forcing software rendering</b></p>"
- "<p>This allows you to use the newer QtWebEngine backend (based on "
- "Chromium) but could have noticeable performance impact (depending on "
- "your hardware). This sets the <i>qt.force_software_rendering = "
- "'chromium'</i> option (if you have a <i>config.py</i> file, you'll "
- "need to set this manually).</p>")
-
def __init__(self, *,
no_err_windows: bool,
save_manager: savemanager.SaveManager) -> None:
self._save_manager = save_manager
self._no_err_windows = no_err_windows
- def _show_dialog(self, *args: typing.Any, **kwargs: typing.Any) -> None:
+ def _show_dialog(self, *args: Any, **kwargs: Any) -> None:
"""Show a dialog for a backend problem."""
if self._no_err_windows:
text = _error_text(*args, **kwargs)
@@ -214,48 +204,7 @@ class _BackendProblemChecker:
self._assert_backend(usertypes.Backend.QtWebEngine)
utils.libgl_workaround()
- def _handle_nouveau_graphics(self) -> None:
- """Force software rendering when using the Nouveau driver.
-
- WORKAROUND for
- https://bugreports.qt.io/browse/QTBUG-41242
- Should be fixed in Qt 5.10 via
- https://codereview.qt-project.org/#/c/208664/
- """
- self._assert_backend(usertypes.Backend.QtWebEngine)
-
- if os.environ.get('QUTE_SKIP_NOUVEAU_CHECK'):
- return
-
- if qtutils.version_check('5.10', compiled=False):
- return
-
- opengl_info = version.opengl_info()
- if opengl_info is None or opengl_info.vendor != 'nouveau':
- return
-
- if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
- # qt.force_software_rendering = 'software-opengl'
- 'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ or
- # qt.force_software_rendering = 'chromium', also see:
- # https://build.opensuse.org/package/view_file/openSUSE:Factory/libqt5-qtwebengine/disable-gpu-when-using-nouveau-boo-1005323.diff?expand=1
- 'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND' in os.environ):
- return
-
- button = _Button("Force software rendering",
- 'qt.force_software_rendering',
- 'chromium')
- self._show_dialog(
- backend=usertypes.Backend.QtWebEngine,
- because="you're using Nouveau graphics",
- text=("<p>There are two ways to fix this:</p>" +
- self.SOFTWARE_RENDERING_TEXT),
- buttons=[button],
- )
-
- raise utils.Unreachable
-
- def _xwayland_options(self) -> typing.Tuple[str, typing.List[_Button]]:
+ def _xwayland_options(self) -> Tuple[str, List[_Button]]:
"""Get buttons/text for a possible XWayland solution."""
buttons = []
text = "<p>You can work around this in one of the following ways:</p>"
@@ -277,36 +226,6 @@ class _BackendProblemChecker:
return text, buttons
- def _handle_wayland(self) -> None:
- self._assert_backend(usertypes.Backend.QtWebEngine)
-
- if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'):
- return
-
- platform = QApplication.instance().platformName()
- if platform not in ['wayland', 'wayland-egl']:
- return
-
- has_qt511 = qtutils.version_check('5.11', compiled=False)
- if has_qt511 and config.val.qt.force_software_rendering == 'chromium':
- return
-
- if qtutils.version_check('5.11.2', compiled=False):
- return
-
- text, buttons = self._xwayland_options()
-
- if has_qt511:
- buttons.append(_Button("Force software rendering",
- 'qt.force_software_rendering',
- 'chromium'))
- text += self.SOFTWARE_RENDERING_TEXT
-
- self._show_dialog(backend=usertypes.Backend.QtWebEngine,
- because="you're using Wayland",
- text=text,
- buttons=buttons)
-
def _handle_wayland_webgl(self) -> None:
"""On older graphic hardware, WebGL on Wayland causes segfaults.
@@ -482,9 +401,7 @@ class _BackendProblemChecker:
# It seems these issues started with Qt 5.12.
# They should be fixed with Qt 5.12.5:
# https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265408
- affected = (qtutils.version_check('5.12', compiled=False) and not
- qtutils.version_check('5.12.5', compiled=False))
- if not affected:
+ if qtutils.version_check('5.12.5', compiled=False):
return
log.init.info("Qt version changed, nuking QtWebEngine cache")
@@ -535,10 +452,8 @@ class _BackendProblemChecker:
self._check_backend_modules()
if objects.backend == usertypes.Backend.QtWebEngine:
self._handle_ssl_support()
- self._handle_wayland()
self._nvidia_shader_workaround()
self._handle_wayland_webgl()
- self._handle_nouveau_graphics()
self._handle_cache_nuking()
self._handle_serviceworker_nuking()
else:
diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py
index 8283dd13e..6f6659a24 100644
--- a/qutebrowser/misc/checkpyver.py
+++ b/qutebrowser/misc/checkpyver.py
@@ -31,7 +31,7 @@ except ImportError: # pragma: no cover
try:
# Python2
from Tkinter import Tk # type: ignore[import, no-redef]
- import tkMessageBox as messagebox # type: ignore # noqa: N813
+ import tkMessageBox as messagebox # type: ignore[import, no-redef] # noqa: N813
except ImportError:
# Some Python without Tk
Tk = None # type: ignore[misc, assignment]
@@ -43,11 +43,11 @@ except ImportError: # pragma: no cover
# to stderr.
def check_python_version():
"""Check if correct python version is run."""
- if sys.hexversion < 0x03050200:
+ if sys.hexversion < 0x03060000:
# We don't use .format() and print_function here just in case someone
# still has < 2.6 installed.
version_str = '.'.join(map(str, sys.version_info[:3]))
- text = ("At least Python 3.5.2 is required to run qutebrowser, but " +
+ text = ("At least Python 3.6 is required to run qutebrowser, but " +
"it's running with " + version_str + ".\n")
if (Tk and # type: ignore[unreachable]
'--no-err-windows' not in sys.argv): # pragma: no cover
diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py
index 810bbd1f9..1403ee56d 100644
--- a/qutebrowser/misc/cmdhistory.py
+++ b/qutebrowser/misc/cmdhistory.py
@@ -19,7 +19,7 @@
"""Command history for the status bar."""
-import typing
+from typing import MutableSequence
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
@@ -60,7 +60,7 @@ class History(QObject):
super().__init__(parent)
self._tmphist = None
if history is None:
- self.history = [] # type: typing.MutableSequence[str]
+ self.history: MutableSequence[str] = []
else:
self.history = history
@@ -82,9 +82,9 @@ class History(QObject):
"""
log.misc.debug("Preset text: '{}'".format(text))
if text:
- items = [
+ items: MutableSequence[str] = [
e for e in self.history
- if e.startswith(text)] # type: typing.MutableSequence[str]
+ if e.startswith(text)]
else:
items = self.history
if not items:
diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py
index aed42237a..14e4a7dc3 100644
--- a/qutebrowser/misc/consolewidget.py
+++ b/qutebrowser/misc/consolewidget.py
@@ -21,7 +21,7 @@
import sys
import code
-import typing
+from typing import MutableSequence
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication
@@ -165,7 +165,7 @@ class ConsoleWidget(QWidget):
'objreg': objreg,
}
self._more = False
- self._buffer = [] # type: typing.MutableSequence[str]
+ self._buffer: MutableSequence[str] = []
self._lineedit = ConsoleLineEdit(namespace, self)
self._lineedit.execute.connect(self.push)
self._output = ConsoleTextEdit()
diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py
index 4387e479a..52cb8ad0c 100644
--- a/qutebrowser/misc/crashdialog.py
+++ b/qutebrowser/misc/crashdialog.py
@@ -28,9 +28,8 @@ import fnmatch
import traceback
import datetime
import enum
-import typing
+from typing import List, Tuple
-import pkg_resources
from PyQt5.QtCore import pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QCheckBox,
@@ -119,7 +118,7 @@ class _CrashDialog(QDialog):
super().__init__(parent)
# We don't set WA_DeleteOnClose here as on an exception, we'll get
# closed anyways, and it only could have unintended side-effects.
- self._crash_info = [] # type: typing.List[typing.Tuple[str, str]]
+ self._crash_info: List[Tuple[str, str]] = []
self._btn_box = None
self._paste_text = None
self.setWindowTitle("Whoops!")
@@ -361,8 +360,8 @@ class _CrashDialog(QDialog):
Args:
newest: The newest version as a string.
"""
- new_version = pkg_resources.parse_version(newest)
- cur_version = pkg_resources.parse_version(qutebrowser.__version__)
+ new_version = utils.parse_version(newest)
+ cur_version = utils.parse_version(qutebrowser.__version__)
lines = ['The report has been sent successfully. Thanks!']
if new_version > cur_version:
lines.append("<b>Note:</b> The newest available version is v{}, "
@@ -635,7 +634,7 @@ class ReportErrorDialog(QDialog):
hbox = QHBoxLayout()
hbox.addStretch()
btn = QPushButton("Close")
- btn.clicked.connect(self.close) # type: ignore[arg-type]
+ btn.clicked.connect(self.close)
hbox.addWidget(btn)
vbox.addLayout(hbox)
diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py
index 3f80db769..d07d8e49c 100644
--- a/qutebrowser/misc/crashsignal.py
+++ b/qutebrowser/misc/crashsignal.py
@@ -29,12 +29,7 @@ import argparse
import functools
import threading
import faulthandler
-import typing
-try:
- # WORKAROUND for segfaults when using pdb in pytest for some reason...
- import readline # pylint: disable=unused-import
-except ImportError:
- pass
+from typing import TYPE_CHECKING, Optional, MutableMapping, cast
import attr
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
@@ -45,7 +40,7 @@ from qutebrowser.api import cmdutils
from qutebrowser.misc import earlyinit, crashdialog, ipc, objects
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils
from qutebrowser.qt import sip
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.misc import quitter
@@ -59,7 +54,7 @@ class ExceptionInfo:
objects = attr.ib()
-crash_handler = typing.cast('CrashHandler', None)
+crash_handler = cast('CrashHandler', None)
class CrashHandler(QObject):
@@ -337,10 +332,9 @@ class SignalHandler(QObject):
self._quitter = quitter
self._notifier = None
self._timer = usertypes.Timer(self, 'python_hacks')
- self._orig_handlers = {
- } # type: typing.MutableMapping[int, signal._HANDLER]
+ self._orig_handlers: MutableMapping[int, 'signal._HANDLER'] = {}
self._activated = False
- self._orig_wakeup_fd = None # type: typing.Optional[int]
+ self._orig_wakeup_fd: Optional[int] = None
def activate(self):
"""Set up signal handlers.
@@ -363,7 +357,7 @@ class SignalHandler(QObject):
for fd in [read_fd, write_fd]:
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
- self._notifier = QSocketNotifier(typing.cast(sip.voidptr, read_fd),
+ self._notifier = QSocketNotifier(cast(sip.voidptr, read_fd),
QSocketNotifier.Read,
self)
self._notifier.activated.connect( # type: ignore[attr-defined]
diff --git a/qutebrowser/misc/debugcachestats.py b/qutebrowser/misc/debugcachestats.py
index 417e4505a..227f9d668 100644
--- a/qutebrowser/misc/debugcachestats.py
+++ b/qutebrowser/misc/debugcachestats.py
@@ -23,17 +23,17 @@ Because many modules depend on this command, this needs to have as few
dependencies as possible to avoid cyclic dependencies.
"""
-import typing
+from typing import Any, Callable, List, Optional, Tuple, TypeVar
# The second element of each tuple should be a lru_cache wrapped function
-_CACHE_FUNCTIONS = [] # type: typing.List[typing.Tuple[str, typing.Any]]
+_CACHE_FUNCTIONS: List[Tuple[str, Any]] = []
-_T = typing.TypeVar('_T', bound=typing.Callable)
+_T = TypeVar('_T', bound=Callable)
-def register(name: typing.Optional[str] = None) -> typing.Callable[[_T], _T]:
+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))
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index c02b2f03c..d1c57760e 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -19,7 +19,7 @@
"""Things which need to be done really early (e.g. before importing Qt).
-At this point we can be sure we have all python 3.5 features available.
+At this point we can be sure we have all python 3.6 features available.
"""
try:
@@ -170,29 +170,21 @@ def qt_version(qversion=None, qt_version_str=None):
def check_qt_version():
"""Check if the Qt version is recent enough."""
- from PyQt5.QtCore import (qVersion, QT_VERSION, PYQT_VERSION,
- PYQT_VERSION_STR)
- from pkg_resources import parse_version
- from qutebrowser.utils import log
- parsed_qversion = parse_version(qVersion())
-
- if (QT_VERSION < 0x050701 or PYQT_VERSION < 0x050700 or
- parsed_qversion < parse_version('5.7.1')):
- text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required, "
+ from PyQt5.QtCore import QT_VERSION, PYQT_VERSION, PYQT_VERSION_STR
+ try:
+ from PyQt5.QtCore import QVersionNumber, QLibraryInfo
+ qt_ver = QLibraryInfo.version().normalized()
+ recent_qt_runtime = qt_ver >= QVersionNumber(5, 12) # type: ignore[operator]
+ except (ImportError, AttributeError):
+ # QVersionNumber was added in Qt 5.6, QLibraryInfo.version() in 5.8
+ recent_qt_runtime = False
+
+ if QT_VERSION < 0x050C00 or PYQT_VERSION < 0x050C00 or not recent_qt_runtime:
+ text = ("Fatal error: Qt >= 5.12.0 and PyQt >= 5.12.0 are required, "
"but Qt {} / PyQt {} is installed.".format(qt_version(),
PYQT_VERSION_STR))
_die(text)
- if qVersion().startswith('5.8.'):
- log.init.warning("Running qutebrowser with Qt 5.8 is untested and "
- "unsupported!")
-
- if (parsed_qversion >= parse_version('5.12') and
- (PYQT_VERSION < 0x050c00 or QT_VERSION < 0x050c00)):
- log.init.warning("Combining PyQt {} with Qt {} is unsupported! Ensure "
- "all versions are newer than 5.12 to avoid potential "
- "issues.".format(PYQT_VERSION_STR, qt_version()))
-
def check_ssl_support():
"""Check if SSL support is available."""
@@ -210,13 +202,13 @@ def _check_modules(modules):
try:
# https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e
# pylint: disable=bad-continuation
- with log.ignore_py_warnings(
+ with log.py_warning_filter(
category=DeprecationWarning,
message=r'invalid escape sequence'
- ), log.ignore_py_warnings(
+ ), log.py_warning_filter(
category=ImportWarning,
message=r'Not importing directory .*: missing __init__'
- ), log.ignore_py_warnings(
+ ), log.py_warning_filter(
category=DeprecationWarning,
message=r'the imp module is deprecated',
):
@@ -251,18 +243,13 @@ def configure_pyqt():
from PyQt5 import QtCore
QtCore.pyqtRemoveInputHook()
try:
- QtCore.pyqt5_enable_new_onexit_scheme( # type: ignore[attr-defined]
- True)
+ QtCore.pyqt5_enable_new_onexit_scheme(True) # type: ignore[attr-defined]
except AttributeError:
# Added in PyQt 5.13 somewhere, going to be the default in 5.14
pass
from qutebrowser.qt import sip
- try:
- # Added in sip 4.19.4
- sip.enableoverflowchecking(True) # type: ignore[attr-defined]
- except AttributeError:
- pass
+ sip.enableoverflowchecking(True)
def init_log(args):
diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py
index 3702715c4..872a594f3 100644
--- a/qutebrowser/misc/guiprocess.py
+++ b/qutebrowser/misc/guiprocess.py
@@ -63,11 +63,11 @@ class GUIProcess(QObject):
self._proc = QProcess(self)
self._proc.errorOccurred.connect(self._on_error)
- self._proc.errorOccurred.connect(self.error) # type: ignore[arg-type]
+ self._proc.errorOccurred.connect(self.error)
self._proc.finished.connect(self._on_finished)
- self._proc.finished.connect(self.finished) # type: ignore[arg-type]
+ self._proc.finished.connect(self.finished)
self._proc.started.connect(self._on_started)
- self._proc.started.connect(self.started) # type: ignore[arg-type]
+ self._proc.started.connect(self.started)
if additional_env is not None:
procenv = QProcessEnvironment.systemEnvironment()
diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py
index 822ba8805..778b53a94 100644
--- a/qutebrowser/misc/httpclient.py
+++ b/qutebrowser/misc/httpclient.py
@@ -19,10 +19,9 @@
"""An HTTP client based on QNetworkAccessManager."""
-import typing
import functools
-import urllib.request
import urllib.parse
+from typing import MutableMapping
from PyQt5.QtCore import pyqtSignal, QObject, QTimer
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
@@ -34,14 +33,8 @@ class HTTPRequest(QNetworkRequest):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- try:
- self.setAttribute(QNetworkRequest.RedirectPolicyAttribute,
- QNetworkRequest.NoLessSafeRedirectPolicy)
- except AttributeError:
- # RedirectPolicyAttribute was introduced in 5.9 to replace
- # FollowRedirectsAttribute.
- self.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
- True)
+ self.setAttribute(QNetworkRequest.RedirectPolicyAttribute,
+ QNetworkRequest.NoLessSafeRedirectPolicy)
class HTTPClient(QObject):
@@ -67,7 +60,7 @@ class HTTPClient(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._nam = QNetworkAccessManager(self)
- self._timers = {} # type: typing.MutableMapping[QNetworkReply, QTimer]
+ self._timers: MutableMapping[QNetworkReply, QTimer] = {}
def post(self, url, data=None):
"""Create a new POST request.
diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py
index 207915a57..e7f370557 100644
--- a/qutebrowser/misc/ipc.py
+++ b/qutebrowser/misc/ipc.py
@@ -196,9 +196,14 @@ class IPCServer(QObject):
self._socket = None
self._old_socket = None
+
if utils.is_windows: # pragma: no cover
- # If we use setSocketOptions on Unix with Qt < 5.4, we get a
- # NameError while listening...
+ # As a WORKAROUND for a Qt bug, we can't use UserAccessOption on Unix. If we
+ # do, we don't get an AddressInUseError anymore:
+ # https://bugreports.qt.io/browse/QTBUG-48635
+ #
+ # Thus, we only do so on Windows, and handle permissions manually in
+ # listen() on Linux.
log.ipc.debug("Calling setSocketOptions")
self._server.setSocketOptions(QLocalServer.UserAccessOption)
else: # pragma: no cover
@@ -222,15 +227,9 @@ class IPCServer(QObject):
if self._server.serverError() == QAbstractSocket.AddressInUseError:
raise AddressInUseError(self._server)
raise ListenError(self._server)
+
if not utils.is_windows: # pragma: no cover
- # If we use setSocketOptions on Unix with Qt < 5.4, we get a
- # NameError while listening.
- # (see b135569d5c6e68c735ea83f42e4baf51f7972281)
- #
- # Also, we don't get an AddressInUseError with Qt 5.5:
- # https://bugreports.qt.io/browse/QTBUG-48635
- #
- # This means we only use setSocketOption on Windows...
+ # WORKAROUND for QTBUG-48635, see the comment in __init__ for details.
try:
os.chmod(self._server.fullServerName(), 0o700)
except FileNotFoundError:
@@ -269,8 +268,8 @@ class IPCServer(QObject):
"No new connection to handle.")
return
log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket)))
- self._timer.start()
self._socket = socket
+ self._timer.start()
socket.readyRead.connect( # type: ignore[attr-defined]
self.on_ready_read)
if socket.canReadLine():
@@ -310,7 +309,7 @@ class IPCServer(QObject):
self._socket.disconnectFromServer()
def _handle_data(self, data):
- """Handle data (as bytes) we got from on_ready_ready_read."""
+ """Handle data (as bytes) we got from on_ready_read."""
try:
decoded = data.decode('utf-8')
except UnicodeDecodeError:
@@ -383,14 +382,14 @@ class IPCServer(QObject):
log.ipc.debug("Read from socket 0x{:x}: {!r}".format(
id(socket), data))
self._handle_data(data)
- self._timer.start()
+
+ if self._socket is not None:
+ self._timer.start()
@pyqtSlot()
def on_timeout(self):
"""Cancel the current connection if it was idle for too long."""
- if self._socket is None: # pragma: no cover
- log.ipc.debug("on_timeout got called with None socket!")
- return
+ assert self._socket is not None
log.ipc.error("IPC connection timed out "
"(socket 0x{:x}).".format(id(self._socket)))
self._socket.disconnectFromServer()
diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py
index 749c5dff1..d3e5a2db0 100644
--- a/qutebrowser/misc/lineparser.py
+++ b/qutebrowser/misc/lineparser.py
@@ -22,7 +22,7 @@
import os
import os.path
import contextlib
-import typing
+from typing import Sequence
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
@@ -150,7 +150,7 @@ class LineParser(BaseLineParser):
"""
super().__init__(configdir, fname, binary=binary, parent=parent)
if not os.path.isfile(self._configfile):
- self.data = [] # type: typing.Sequence[str]
+ self.data: Sequence[str] = []
else:
log.init.debug("Reading {}".format(self._configfile))
self._read()
diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py
index 2310a1926..58bdf374d 100644
--- a/qutebrowser/misc/miscwidgets.py
+++ b/qutebrowser/misc/miscwidgets.py
@@ -19,7 +19,7 @@
"""Misc. widgets used at different places."""
-import typing
+from typing import Optional
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QTimer
from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
@@ -239,8 +239,8 @@ class WrapperLayout(QLayout):
def __init__(self, parent=None):
super().__init__(parent)
- self._widget = None # type: typing.Optional[QWidget]
- self._container = None # type: typing.Optional[QWidget]
+ self._widget: Optional[QWidget] = None
+ self._container: Optional[QWidget] = None
def addItem(self, _widget):
raise utils.Unreachable
@@ -284,47 +284,6 @@ class WrapperLayout(QLayout):
self._container.setFocusProxy(None) # type: ignore[arg-type]
-class PseudoLayout(QLayout):
-
- """A layout which isn't actually a real layout.
-
- This is used to replace QWebEngineView's internal layout, as a WORKAROUND
- for https://bugreports.qt.io/browse/QTBUG-68224 and other related issues.
-
- This is partly inspired by https://codereview.qt-project.org/#/c/230894/
- which does something similar as part of Qt.
- """
-
- def addItem(self, item):
- assert self.parent() is not None
- item.widget().setParent(self.parent())
-
- def removeItem(self, item):
- item.widget().setParent(None)
-
- def count(self):
- return 0
-
- def itemAt(self, _pos):
- return None
-
- def widget(self):
- return self.parent().render_widget()
-
- def setGeometry(self, rect):
- """Resize the render widget when the view is resized."""
- widget = self.widget()
- if widget is not None:
- widget.setGeometry(rect)
-
- def sizeHint(self):
- """Make sure the view has the sizeHint of the render widget."""
- widget = self.widget()
- if widget is not None:
- return widget.sizeHint()
- return QSize()
-
-
class FullscreenNotification(QLabel):
"""A label telling the user this page is now fullscreen."""
@@ -390,10 +349,10 @@ class InspectorSplitter(QSplitter):
self.addWidget(main_webview)
self.setFocusProxy(main_webview)
self.splitterMoved.connect(self._on_splitter_moved)
- self._main_idx = None # type: typing.Optional[int]
- self._inspector_idx = None # type: typing.Optional[int]
- self._position = None # type: typing.Optional[inspector.Position]
- self._preferred_size = None # type: typing.Optional[int]
+ self._main_idx: Optional[int] = None
+ self._inspector_idx: Optional[int] = None
+ self._position: Optional[inspector.Position] = None
+ self._preferred_size: Optional[int] = None
def cycle_focus(self):
"""Cycle keyboard focus between the main/inspector widget."""
diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py
index 28a1830d5..c2e20e9ad 100644
--- a/qutebrowser/misc/objects.py
+++ b/qutebrowser/misc/objects.py
@@ -22,10 +22,10 @@
# NOTE: We need to be careful with imports here, as this is imported from
# earlyinit.
-import typing
import argparse
+from typing import TYPE_CHECKING, Any, Dict, Set, Union, cast
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.utils import usertypes
from qutebrowser.commands import command
@@ -38,11 +38,11 @@ class NoBackend:
def name(self) -> str:
raise AssertionError("No backend set!")
- def __eq__(self, other: typing.Any) -> bool:
+ def __eq__(self, other: Any) -> bool:
raise AssertionError("No backend set!")
-backend = NoBackend() # type: typing.Union[usertypes.Backend, NoBackend]
-commands = {} # type: typing.Dict[str, command.Command]
-debug_flags = set() # type: typing.Set[str]
-args = typing.cast(argparse.Namespace, None)
+backend: Union['usertypes.Backend', NoBackend] = NoBackend()
+commands: Dict[str, 'command.Command'] = {}
+debug_flags: Set[str] = set()
+args = cast(argparse.Namespace, None)
diff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py
index c7f0c8072..86342d57b 100644
--- a/qutebrowser/misc/quitter.py
+++ b/qutebrowser/misc/quitter.py
@@ -25,11 +25,11 @@ import sys
import json
import atexit
import shutil
-import typing
import argparse
import tokenize
import functools
import subprocess
+from typing import Iterable, Mapping, MutableSequence, Sequence, cast
from PyQt5.QtCore import QObject, pyqtSignal, QTimer
from PyQt5.QtWidgets import QApplication
@@ -46,7 +46,7 @@ from qutebrowser.mainwindow import prompt
from qutebrowser.completion.models import miscmodels
-instance = typing.cast('Quitter', None)
+instance = cast('Quitter', None)
class Quitter(QObject):
@@ -97,10 +97,10 @@ class Quitter(QObject):
compile(f.read(), fn, 'exec')
def _get_restart_args(
- self, pages: typing.Iterable[str] = (),
+ self, pages: Iterable[str] = (),
session: str = None,
- override_args: typing.Mapping[str, str] = None
- ) -> typing.Sequence[str]:
+ override_args: Mapping[str, str] = None
+ ) -> Sequence[str]:
"""Get args to relaunch qutebrowser.
Args:
@@ -120,7 +120,7 @@ class Quitter(QObject):
args = [sys.executable, '-m', 'qutebrowser']
# Add all open pages so they get reopened.
- page_args = [] # type: typing.MutableSequence[str]
+ page_args: MutableSequence[str] = []
for win in pages:
page_args.extend(win)
page_args.append('')
@@ -157,9 +157,9 @@ class Quitter(QObject):
return args
- def restart(self, pages: typing.Sequence[str] = (),
+ def restart(self, pages: Sequence[str] = (),
session: str = None,
- override_args: typing.Mapping[str, str] = None) -> bool:
+ override_args: Mapping[str, str] = None) -> bool:
"""Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as
diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py
index daa484934..c7a20adbb 100644
--- a/qutebrowser/misc/savemanager.py
+++ b/qutebrowser/misc/savemanager.py
@@ -21,7 +21,7 @@
import os.path
import collections
-import typing
+from typing import MutableMapping
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
@@ -112,8 +112,7 @@ class SaveManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
- self.saveables = collections.OrderedDict(
- ) # type: typing.MutableMapping[str, Saveable]
+ self.saveables: MutableMapping[str, Saveable] = collections.OrderedDict()
self._save_timer = usertypes.Timer(self, name='save-timer')
self._save_timer.timeout.connect(self.autosave)
self._set_autosave_interval()
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index dcdc0821b..b4aa72f32 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -23,9 +23,9 @@ import os
import os.path
import itertools
import urllib
-import typing
import glob
import shutil
+from typing import Any, Iterable, MutableMapping, MutableSequence, Optional, Union, cast
from PyQt5.QtCore import Qt, QUrl, QObject, QPoint, QTimer, QDateTime
from PyQt5.QtWidgets import QApplication
@@ -40,7 +40,7 @@ from qutebrowser.mainwindow import mainwindow
from qutebrowser.qt import sip
-_JsonType = typing.MutableMapping[str, typing.Any]
+_JsonType = MutableMapping[str, Any]
class Sentinel:
@@ -49,9 +49,9 @@ class Sentinel:
default = Sentinel()
-session_manager = typing.cast('SessionManager', None)
+session_manager = cast('SessionManager', None)
-ArgType = typing.Union[str, Sentinel]
+ArgType = Union[str, Sentinel]
def init(parent=None):
@@ -80,10 +80,10 @@ def init(parent=None):
session_manager = SessionManager(base_path, parent)
-def shutdown(session: typing.Optional[ArgType], last_window: bool) -> None:
+def shutdown(session: Optional[ArgType], last_window: bool) -> None:
"""Handle a shutdown by saving sessions and removing the autosave file."""
if session_manager is None:
- return # type: ignore
+ return # type: ignore[unreachable]
try:
if session is not None:
@@ -153,7 +153,7 @@ class SessionManager(QObject):
def __init__(self, base_path, parent=None):
super().__init__(parent)
- self.current = None # type: typing.Optional[str]
+ self.current: Optional[str] = None
self._base_path = base_path
self._last_window_session = None
self.did_load = False
@@ -196,9 +196,9 @@ class SessionManager(QObject):
Return:
A dict with the saved data for this item.
"""
- data = {
+ data: _JsonType = {
'url': bytes(item.url().toEncoded()).decode('ascii'),
- } # type: _JsonType
+ }
if item.title():
data['title'] = item.title()
@@ -246,7 +246,7 @@ class SessionManager(QObject):
tab: The WebView to save.
active: Whether the tab is currently active.
"""
- data = {'history': []} # type: _JsonType
+ data: _JsonType = {'history': []}
if active:
data['active'] = True
for idx, item in enumerate(tab.history):
@@ -263,9 +263,9 @@ class SessionManager(QObject):
def _save_all(self, *, only_window=None, with_private=False):
"""Get a dict with data for all windows/tabs."""
- data = {'windows': []} # type: _JsonType
+ data: _JsonType = {'windows': []}
if only_window is not None:
- winlist = [only_window] # type: typing.Iterable[int]
+ winlist: Iterable[int] = [only_window]
else:
winlist = objreg.window_registry
@@ -282,7 +282,7 @@ class SessionManager(QObject):
if tabbed_browser.is_private and not with_private:
continue
- win_data = {} # type: _JsonType
+ win_data: _JsonType = {}
active_window = QApplication.instance().activeWindow()
if getattr(active_window, 'win_id', None) == win_id:
win_data['active'] = True
@@ -377,7 +377,7 @@ class SessionManager(QObject):
def _load_tab(self, new_tab, data): # noqa: C901
"""Load yaml data into a newly opened tab."""
entries = []
- lazy_load = [] # type: typing.MutableSequence[_JsonType]
+ lazy_load: MutableSequence[_JsonType] = []
# use len(data['history'])
# -> dropwhile empty if not session.lazy_session
lazy_index = len(data['history'])
@@ -436,10 +436,10 @@ class SessionManager(QObject):
orig_url = url
if histentry.get("last_visited"):
- last_visited = QDateTime.fromString(
+ last_visited: Optional[QDateTime] = QDateTime.fromString(
histentry.get("last_visited"),
Qt.ISODate,
- ) # type: typing.Optional[QDateTime]
+ )
else:
last_visited = None
@@ -470,8 +470,7 @@ class SessionManager(QObject):
if tab.get('active', False):
tab_to_focus = i
if new_tab.data.pinned:
- tabbed_browser.widget.set_tab_pinned(new_tab,
- new_tab.data.pinned)
+ new_tab.set_pinned(True)
if tab_to_focus is not None:
tabbed_browser.widget.setCurrentIndex(tab_to_focus)
if win.get('active', False):
diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py
index 8f9265419..2af95ac15 100644
--- a/qutebrowser/misc/sql.py
+++ b/qutebrowser/misc/sql.py
@@ -35,7 +35,6 @@ class SqliteErrorCode:
in qutebrowser here.
"""
- UNKNOWN = '-1'
ERROR = '1' # generic error code
BUSY = '5' # database is locked
READONLY = '8' # attempt to write a readonly database
@@ -108,13 +107,6 @@ def raise_sqlite_error(msg, error):
SqliteErrorCode.NOTADB,
]
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70506
- # We don't know what the actual error was, but let's assume it's not us to
- # blame... Usually this is something like an unreadable database file.
- qtbug_70506 = (error_code == SqliteErrorCode.UNKNOWN and
- driver_text == "Error opening database" and
- database_text == "out of memory")
-
# https://github.com/qutebrowser/qutebrowser/issues/4681
# If the query we built was too long
too_long_err = (
@@ -123,7 +115,7 @@ def raise_sqlite_error(msg, error):
database_text in ["too many SQL variables",
"LIKE or GLOB pattern too complex"]))
- if error_code in known_errors or qtbug_70506 or too_long_err:
+ if error_code in known_errors or too_long_err:
raise KnownError(msg, error)
raise BugError(msg, error)
diff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py
index a8d24bd12..3540d8824 100644
--- a/qutebrowser/misc/throttle.py
+++ b/qutebrowser/misc/throttle.py
@@ -19,8 +19,8 @@
"""A throttle for throttling function calls."""
-import typing
import time
+from typing import Any, Callable, Mapping, Optional, Sequence
import attr
from PyQt5.QtCore import QObject
@@ -31,8 +31,8 @@ from qutebrowser.utils import usertypes
@attr.s
class _CallArgs:
- args = attr.ib() # type: typing.Sequence[typing.Any]
- kwargs = attr.ib() # type: typing.Mapping[str, typing.Any]
+ args: Sequence[Any] = attr.ib()
+ kwargs: Mapping[str, Any] = attr.ib()
class Throttle(QObject):
@@ -45,7 +45,7 @@ class Throttle(QObject):
"""
def __init__(self,
- func: typing.Callable,
+ func: Callable,
delay_ms: int,
parent: QObject = None) -> None:
"""Constructor.
@@ -59,8 +59,8 @@ class Throttle(QObject):
super().__init__(parent)
self._delay_ms = delay_ms
self._func = func
- self._pending_call = None # type: typing.Optional[_CallArgs]
- self._last_call_ms = None # type: typing.Optional[int]
+ self._pending_call: Optional[_CallArgs] = None
+ self._last_call_ms: Optional[int] = None
self._timer = usertypes.Timer(self, 'throttle-timer')
self._timer.setSingleShot(True)
@@ -71,7 +71,7 @@ class Throttle(QObject):
self._pending_call = None
self._last_call_ms = int(time.monotonic() * 1000)
- def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
cur_time_ms = int(time.monotonic() * 1000)
if self._pending_call is None:
if (self._last_call_ms is None or
diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py
index 8c2462b2b..56138c798 100644
--- a/qutebrowser/misc/utilcmds.py
+++ b/qutebrowser/misc/utilcmds.py
@@ -24,7 +24,7 @@
import functools
import os
import traceback
-import typing
+from typing import Optional
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
@@ -274,7 +274,7 @@ def version(win_id: int, paste: bool = False) -> None:
pastebin_version()
-_keytester_widget = None # type: typing.Optional[miscwidgets.KeyTesterWidget]
+_keytester_widget: Optional[miscwidgets.KeyTesterWidget] = None
@cmdutils.register(debug=True)
diff --git a/qutebrowser/qt.py b/qutebrowser/qt.py
index 5b44530bb..a9cd8ed90 100644
--- a/qutebrowser/qt.py
+++ b/qutebrowser/qt.py
@@ -20,9 +20,4 @@
"""Wrappers around Qt/PyQt code."""
# pylint: disable=unused-import
-# PyQt 5.11 comes with a bundled sip,
-# for older PyQt versions it's a separate module.
-try:
- from PyQt5 import sip
-except ImportError:
- import sip # type: ignore[import, no-redef]
+from PyQt5 import sip
diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py
index 2a830e8ac..7f36d4807 100644
--- a/qutebrowser/qutebrowser.py
+++ b/qutebrowser/qutebrowser.py
@@ -82,14 +82,6 @@ def get_argparser():
"qutebrowser instance running.")
parser.add_argument('--backend', choices=['webkit', 'webengine'],
help="Which backend to use.")
- parser.add_argument('--enable-webengine-inspector', action='store_true',
- help="Enable the web inspector / devtools for "
- "QtWebEngine. Note that this is a SECURITY RISK and "
- "you should not visit untrusted websites with the "
- "inspector turned on. See "
- "https://bugreports.qt.io/browse/QTBUG-50725 for more "
- "details. This is not needed anymore since Qt 5.11 "
- "where the inspector is always enabled and secure.")
parser.add_argument('--json-args', help=argparse.SUPPRESS)
parser.add_argument('--temp-basedir-restarted', help=argparse.SUPPRESS)
@@ -175,12 +167,14 @@ def debug_flag_error(flag):
log-scroll-pos: Log all scrolling changes.
stack: Enable Chromium stack logging.
chromium: Enable Chromium logging.
+ wait-renderer-process: Wait for debugger in renderer process.
+ avoid-chromium-init: Enable `--version` without initializing Chromium.
werror: Turn Python warnings into errors.
"""
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',
'no-scroll-filtering', 'log-requests', 'log-cookies',
- 'lost-focusproxy', 'log-scroll-pos', 'stack', 'chromium',
- 'werror']
+ 'log-scroll-pos', 'stack', 'chromium',
+ 'wait-renderer-process', 'avoid-chromium-init', 'werror']
if flag in valid_flags:
return flag
diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py
index 66cfeed9e..229a26ead 100644
--- a/qutebrowser/utils/debug.py
+++ b/qutebrowser/utils/debug.py
@@ -24,22 +24,23 @@ import inspect
import logging
import functools
import datetime
-import typing
import types
+from typing import (
+ Any, Callable, List, Mapping, MutableSequence, Optional, Sequence, Type, Union)
-from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtSignal
+from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSignal
from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import log, utils, qtutils, objreg
from qutebrowser.qt import sip
-def log_events(klass: typing.Type) -> typing.Type:
+def log_events(klass: Type) -> Type:
"""Class decorator to log Qt events."""
old_event = klass.event
@functools.wraps(old_event)
- def new_event(self: typing.Any, e: QEvent) -> bool:
+ def new_event(self: Any, e: QEvent) -> bool:
"""Wrapper for event() which logs events."""
log.misc.debug("Event in {}: {}".format(utils.qualname(klass),
qenum_key(QEvent, e.type())))
@@ -54,7 +55,7 @@ def log_signals(obj: QObject) -> QObject:
Can be used as class decorator.
"""
- def log_slot(obj: QObject, signal: pyqtSignal, *args: typing.Any) -> None:
+ def log_slot(obj: QObject, signal: pyqtBoundSignal, *args: Any) -> None:
"""Slot connected to a signal to log it."""
dbg = dbg_signal(signal, args)
try:
@@ -83,9 +84,7 @@ def log_signals(obj: QObject) -> QObject:
old_init = obj.__init__ # type: ignore[misc]
@functools.wraps(old_init)
- def new_init(self: typing.Any,
- *args: typing.Any,
- **kwargs: typing.Any) -> None:
+ def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
"""Wrapper for __init__() which logs signals."""
old_init(self, *args, **kwargs)
connect_log_slot(self)
@@ -97,10 +96,10 @@ def log_signals(obj: QObject) -> QObject:
return obj
-def qenum_key(base: typing.Type,
- value: typing.Union[int, sip.simplewrapper],
+def qenum_key(base: Type,
+ value: Union[int, sip.simplewrapper],
add_base: bool = False,
- klass: typing.Type = None) -> str:
+ klass: Type = None) -> str:
"""Convert a Qt Enum value to its key as a string.
Args:
@@ -140,10 +139,10 @@ def qenum_key(base: typing.Type,
return ret
-def qflags_key(base: typing.Type,
- value: typing.Union[int, sip.simplewrapper],
+def qflags_key(base: Type,
+ value: Union[int, sip.simplewrapper],
add_base: bool = False,
- klass: typing.Type = None) -> str:
+ klass: Type = None) -> str:
"""Convert a Qt QFlags value to its keys as string.
Note: Passing a combined value (such as Qt.AlignCenter) will get the names
@@ -188,18 +187,17 @@ def qflags_key(base: typing.Type,
return '|'.join(names)
-def signal_name(sig: pyqtSignal) -> str:
+def signal_name(sig: pyqtBoundSignal) -> str:
"""Get a cleaned up name of a signal.
- Unfortunately, the way to get the name of a signal differs based on:
- - PyQt versions (5.11 added .signatures for unbound signals)
- - Bound vs. unbound signals
+ Unfortunately, the way to get the name of a signal differs based on
+ bound vs. unbound signals.
Here, we try to get the name from .signal or .signatures, or if all else
fails, extract it from the repr().
Args:
- sig: The pyqtSignal
+ sig: A bound signal.
Return:
The cleaned up signal name.
@@ -209,37 +207,20 @@ def signal_name(sig: pyqtSignal) -> str:
# Examples:
# sig.signal == '2signal1'
# sig.signal == '2signal2(QString,QString)'
- m = re.fullmatch(r'[0-9]+(?P<name>.*)\(.*\)',
- sig.signal) # type: ignore[attr-defined]
- elif hasattr(sig, 'signatures'):
+ m = re.fullmatch(r'[0-9]+(?P<name>.*)\(.*\)', sig.signal)
+ else:
# Unbound signal, PyQt >= 5.11
# Examples:
# sig.signatures == ('signal1()',)
# sig.signatures == ('signal2(QString,QString)',)
m = re.fullmatch(r'(?P<name>.*)\(.*\)',
sig.signatures[0]) # type: ignore[attr-defined]
- else: # pragma: no cover
- # Unbound signal, PyQt < 5.11
- # Examples:
- # repr(sig) == "<unbound PYQT_SIGNAL SignalObject.signal1[]>"
- # repr(sig) == "<unbound PYQT_SIGNAL SignalObject.signal2[str, str]>"
- # repr(sig) == "<unbound PYQT_SIGNAL timeout()>"
- # repr(sig) == "<unbound PYQT_SIGNAL valueChanged(int)>"
- patterns = [
- r'<unbound PYQT_SIGNAL [^.]*\.(?P<name>[^[]*)\[.*>',
- r'<unbound PYQT_SIGNAL (?P<name>[^(]*)\(.*>',
- ]
- for pattern in patterns:
- m = re.fullmatch(pattern, repr(sig))
- if m is not None:
- break
assert m is not None, sig
return m.group('name')
-def format_args(args: typing.Sequence = None,
- kwargs: typing.Mapping = None) -> str:
+def format_args(args: Sequence = None, kwargs: Mapping = None) -> str:
"""Format a list of arguments/kwargs to a function-call like string."""
if args is not None:
arglist = [utils.compact_text(repr(arg), 200) for arg in args]
@@ -251,11 +232,11 @@ def format_args(args: typing.Sequence = None,
return ', '.join(arglist)
-def dbg_signal(sig: pyqtSignal, args: typing.Any) -> str:
+def dbg_signal(sig: pyqtBoundSignal, args: Any) -> str:
"""Get a string representation of a signal for debugging.
Args:
- sig: A pyqtSignal.
+ sig: A bound signal.
args: The arguments as list of strings.
Return:
@@ -264,9 +245,9 @@ def dbg_signal(sig: pyqtSignal, args: typing.Any) -> str:
return '{}({})'.format(signal_name(sig), format_args(args))
-def format_call(func: typing.Callable,
- args: typing.Sequence = None,
- kwargs: typing.Mapping = None,
+def format_call(func: Callable,
+ args: Sequence = None,
+ kwargs: Mapping = None,
full: bool = True) -> str:
"""Get a string representation of a function calls with the given args.
@@ -293,7 +274,7 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
Usable as context manager or as decorator.
"""
- def __init__(self, logger: typing.Union[logging.Logger, str],
+ def __init__(self, logger: Union[logging.Logger, str],
action: str = 'operation') -> None:
"""Constructor.
@@ -305,28 +286,25 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
self._logger = logging.getLogger(logger)
else:
self._logger = logger
- self._started = None # type: typing.Optional[datetime.datetime]
+ self._started: Optional[datetime.datetime] = None
self._action = action
def __enter__(self) -> None:
self._started = datetime.datetime.now()
- # The string annotation is a WORKAROUND for a Python 3.5.2 bug:
- # https://github.com/python/typing/issues/266
-
def __exit__(self,
- _exc_type: 'typing.Optional[typing.Type[BaseException]]',
- _exc_val: typing.Optional[BaseException],
- _exc_tb: typing.Optional[types.TracebackType]) -> None:
+ _exc_type: Optional[Type[BaseException]],
+ _exc_val: Optional[BaseException],
+ _exc_tb: Optional[types.TracebackType]) -> None:
assert self._started is not None
finished = datetime.datetime.now()
delta = (finished - self._started).total_seconds()
self._logger.debug("{} took {} seconds.".format(
self._action.capitalize(), delta))
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
@functools.wraps(func)
- def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
"""Call the original function."""
with self:
return func(*args, **kwargs)
@@ -334,14 +312,14 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
return wrapped
-def _get_widgets() -> typing.Sequence[str]:
+def _get_widgets() -> Sequence[str]:
"""Get a string list of all widgets."""
widgets = QApplication.instance().allWidgets()
widgets.sort(key=repr)
return [repr(w) for w in widgets]
-def _get_pyqt_objects(lines: typing.MutableSequence[str],
+def _get_pyqt_objects(lines: MutableSequence[str],
obj: QObject,
depth: int = 0) -> None:
"""Recursive method for get_all_objects to get Qt objects."""
@@ -362,7 +340,7 @@ def get_all_objects(start_obj: QObject = None) -> str:
if start_obj is None:
start_obj = QApplication.instance()
- pyqt_lines = [] # type: typing.List[str]
+ pyqt_lines: List[str] = []
_get_pyqt_objects(pyqt_lines, start_obj)
pyqt_lines = [' ' + e for e in pyqt_lines]
pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(len(pyqt_lines)))
diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py
index 0ef971dfc..f1ee11f84 100644
--- a/qutebrowser/utils/docutils.py
+++ b/qutebrowser/utils/docutils.py
@@ -25,7 +25,7 @@ import inspect
import os.path
import collections
import enum
-import typing
+from typing import Callable, MutableMapping, Optional, List, Union
import qutebrowser
from qutebrowser.utils import log, utils
@@ -77,21 +77,29 @@ class DocstringParser:
arg_descs: A dict of argument names to their descriptions
"""
- State = enum.Enum('State', ['short', 'desc', 'desc_hidden',
- 'arg_start', 'arg_inside', 'misc'])
+ class State(enum.Enum):
- def __init__(self, func: typing.Callable) -> None:
+ """The current state of the parser."""
+
+ short = enum.auto()
+ desc = enum.auto()
+ desc_hidden = enum.auto()
+ arg_start = enum.auto()
+ arg_inside = enum.auto()
+ misc = enum.auto()
+
+ def __init__(self, func: Callable) -> None:
"""Constructor.
Args:
func: The function to parse the docstring for.
"""
self._state = self.State.short
- self._cur_arg_name = None # type: typing.Optional[str]
- self._short_desc_parts = [] # type: typing.List[str]
- self._long_desc_parts = [] # type: typing.List[str]
- self.arg_descs = collections.OrderedDict(
- ) # type: typing.Dict[str, typing.Union[str, typing.List[str]]]
+ self._cur_arg_name: Optional[str] = None
+ self._short_desc_parts: List[str] = []
+ self._long_desc_parts: List[str] = []
+ self.arg_descs: MutableMapping[
+ str, Union[str, List[str]]] = collections.OrderedDict()
doc = inspect.getdoc(func)
handlers = {
self.State.short: self._parse_short,
diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py
index 1b499a377..94068c640 100644
--- a/qutebrowser/utils/javascript.py
+++ b/qutebrowser/utils/javascript.py
@@ -19,10 +19,10 @@
"""Utilities related to javascript interaction."""
-import typing
+from typing import Sequence, Union
-_InnerJsArgType = typing.Union[None, str, bool, int, float]
-_JsArgType = typing.Union[_InnerJsArgType, typing.Sequence[_InnerJsArgType]]
+_InnerJsArgType = Union[None, str, bool, int, float]
+_JsArgType = Union[_InnerJsArgType, Sequence[_InnerJsArgType]]
def string_escape(text: str) -> str:
diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py
index 78663645d..f60c46bbc 100644
--- a/qutebrowser/utils/jinja.py
+++ b/qutebrowser/utils/jinja.py
@@ -21,10 +21,10 @@
import os
import os.path
-import typing
import functools
import contextlib
import html
+from typing import Any, Callable, FrozenSet, Iterator, List, Set, Tuple
import jinja2
import jinja2.nodes
@@ -68,7 +68,7 @@ class Loader(jinja2.BaseLoader):
self,
_env: jinja2.Environment,
template: str
- ) -> typing.Tuple[str, str, typing.Callable[[], bool]]:
+ ) -> Tuple[str, str, Callable[[], bool]]:
path = os.path.join(self._subdir, template)
try:
source = utils.read_file(path)
@@ -98,7 +98,7 @@ class Environment(jinja2.Environment):
self._autoescape = True
@contextlib.contextmanager
- def no_autoescape(self) -> typing.Iterator[None]:
+ def no_autoescape(self) -> Iterator[None]:
"""Context manager to temporarily turn off autoescaping."""
self._autoescape = False
yield
@@ -122,7 +122,7 @@ class Environment(jinja2.Environment):
mimetype = utils.guess_mimetype(filename)
return urlutils.data_url(mimetype, data).toString()
- def getattr(self, obj: typing.Any, attribute: str) -> typing.Any:
+ def getattr(self, obj: Any, attribute: str) -> Any:
"""Override jinja's getattr() to be less clever.
This means it doesn't fall back to __getitem__, and it doesn't hide
@@ -131,7 +131,7 @@ class Environment(jinja2.Environment):
return getattr(obj, attribute)
-def render(template: str, **kwargs: typing.Any) -> str:
+def render(template: str, **kwargs: Any) -> str:
"""Render the given template and pass the given arguments to it."""
return environment.get_template(template).render(**kwargs)
@@ -142,10 +142,10 @@ js_environment = jinja2.Environment(loader=Loader('javascript'))
@debugcachestats.register()
@functools.lru_cache()
-def template_config_variables(template: str) -> typing.FrozenSet[str]:
+def template_config_variables(template: str) -> FrozenSet[str]:
"""Return the config variables used in the template."""
unvisted_nodes = [environment.parse(template)]
- result = set() # type: typing.Set[str]
+ result: Set[str] = set()
while unvisted_nodes:
node = unvisted_nodes.pop()
if not isinstance(node, jinja2.nodes.Getattr):
@@ -154,7 +154,7 @@ def template_config_variables(template: str) -> typing.FrozenSet[str]:
# List of attribute names in reverse order.
# For example it's ['ab', 'c', 'd'] for 'conf.d.c.ab'.
- attrlist = [] # type: typing.List[str]
+ attrlist: List[str] = []
while isinstance(node, jinja2.nodes.Getattr):
attrlist.append(node.attr) # type: ignore[attr-defined]
node = node.node # type: ignore[attr-defined]
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 165e5143f..9aedb419f 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -31,8 +31,9 @@ import traceback
import warnings
import json
import inspect
-import typing
import argparse
+from typing import (TYPE_CHECKING, Any, Iterator, Mapping, MutableSequence,
+ Optional, Set, Tuple, Union, cast)
from PyQt5 import QtCore
# Optional imports
@@ -41,7 +42,7 @@ try:
except ImportError:
colorama = None
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
from qutebrowser.config import config as configmodule
_log_inited = False
@@ -98,8 +99,8 @@ LOG_LEVELS = {
def vdebug(self: logging.Logger,
msg: str,
- *args: typing.Any,
- **kwargs: typing.Any) -> None:
+ *args: Any,
+ **kwargs: Any) -> None:
"""Log with a VDEBUG level.
VDEBUG is used when a debug message is rather verbose, and probably of
@@ -159,8 +160,8 @@ LOGGER_NAMES = [
]
-ram_handler = None # type: typing.Optional[RAMHandler]
-console_handler = None # type: typing.Optional[logging.Handler]
+ram_handler: Optional['RAMHandler'] = None
+console_handler: Optional[logging.Handler] = None
console_filter = None
@@ -233,19 +234,9 @@ def _init_py_warnings() -> None:
@contextlib.contextmanager
-def disable_qt_msghandler() -> typing.Iterator[None]:
- """Contextmanager which temporarily disables the Qt message handler."""
- old_handler = QtCore.qInstallMessageHandler(None)
- try:
- yield
- finally:
- QtCore.qInstallMessageHandler(old_handler)
-
-
-@contextlib.contextmanager
-def ignore_py_warnings(**kwargs: typing.Any) -> typing.Iterator[None]:
+def py_warning_filter(action: str = 'ignore', **kwargs: Any) -> Iterator[None]:
"""Contextmanager to temporarily disable certain Python warnings."""
- warnings.filterwarnings('ignore', **kwargs)
+ warnings.filterwarnings(action, **kwargs)
yield
if _log_inited:
_init_py_warnings()
@@ -257,7 +248,7 @@ def _init_handlers(
force_color: bool,
json_logging: bool,
ram_capacity: int
-) -> typing.Tuple[logging.StreamHandler, typing.Optional['RAMHandler']]:
+) -> Tuple[logging.StreamHandler, Optional['RAMHandler']]:
"""Init log handlers.
Args:
@@ -311,8 +302,8 @@ def _init_formatters(
color: bool,
force_color: bool,
json_logging: bool
-) -> typing.Tuple[typing.Union['JSONFormatter', 'ColoredFormatter'],
- 'ColoredFormatter', 'HTMLFormatter', bool]:
+) -> Tuple[Union['JSONFormatter', 'ColoredFormatter'],
+ 'ColoredFormatter', 'HTMLFormatter', bool]:
"""Init log formatters.
Args:
@@ -364,7 +355,7 @@ def change_console_formatter(level: int) -> None:
"""
assert console_handler is not None
- old_formatter = typing.cast(ColoredFormatter, console_handler.formatter)
+ old_formatter = cast(ColoredFormatter, console_handler.formatter)
console_fmt = get_console_format(level)
console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{',
use_colors=old_formatter.use_colors)
@@ -393,7 +384,9 @@ def qt_message_handler(msg_type: QtCore.QtMsgType,
try:
qt_to_logging[QtCore.QtInfoMsg] = logging.INFO
except AttributeError:
- # While we don't support Qt < 5.5 anymore, logging still needs to work
+ # Added in Qt 5.5.
+ # While we don't support Qt < 5.5 anymore, logging still needs to work so that
+ # the Qt version warning in earlyinit.py does.
pass
# Change levels of some well-known messages to debug so they don't get
@@ -504,7 +497,7 @@ def qt_message_handler(msg_type: QtCore.QtMsgType,
assert _args is not None
if _args.debug:
- stack = ''.join(traceback.format_stack()) # type: typing.Optional[str]
+ stack: Optional[str] = ''.join(traceback.format_stack())
else:
stack = None
@@ -515,7 +508,7 @@ def qt_message_handler(msg_type: QtCore.QtMsgType,
@contextlib.contextmanager
-def hide_qt_warning(pattern: str, logger: str = 'qt') -> typing.Iterator[None]:
+def hide_qt_warning(pattern: str, logger: str = 'qt') -> Iterator[None]:
"""Hide Qt warnings matching the given regex."""
log_filter = QtWarningFilter(pattern)
logger_obj = logging.getLogger(logger)
@@ -578,7 +571,7 @@ class InvalidLogFilterError(Exception):
"""Raised when an invalid filter string is passed to LogFilter.parse()."""
- def __init__(self, names: typing.Set[str]):
+ def __init__(self, names: Set[str]):
invalid = names - set(LOGGER_NAMES)
super().__init__("Invalid log category {} - valid categories: {}"
.format(', '.join(sorted(invalid)),
@@ -599,7 +592,7 @@ class LogFilter(logging.Filter):
than debug.
"""
- def __init__(self, names: typing.Set[str], *, negated: bool = False,
+ def __init__(self, names: Set[str], *, negated: bool = False,
only_debug: bool = True) -> None:
super().__init__()
self.names = names
@@ -607,7 +600,7 @@ class LogFilter(logging.Filter):
self.only_debug = only_debug
@classmethod
- def parse(cls, filter_str: typing.Optional[str], *,
+ def parse(cls, filter_str: Optional[str], *,
only_debug: bool = True) -> 'LogFilter':
"""Parse a log filter from a string."""
if filter_str is None or filter_str == 'none':
@@ -661,11 +654,11 @@ class RAMHandler(logging.Handler):
def __init__(self, capacity: int) -> None:
super().__init__()
- self.html_formatter = None # type: typing.Optional[HTMLFormatter]
+ self.html_formatter: Optional[HTMLFormatter] = None
if capacity != -1:
- self._data = collections.deque(
+ self._data: MutableSequence[logging.LogRecord] = collections.deque(
maxlen=capacity
- ) # type: typing.MutableSequence[logging.LogRecord]
+ )
else:
self._data = collections.deque()
@@ -748,9 +741,7 @@ class HTMLFormatter(logging.Formatter):
_colordict: The colordict passed to the logger.
"""
- def __init__(self, fmt: str,
- datefmt: str,
- log_colors: typing.Mapping[str, str]) -> None:
+ def __init__(self, fmt: str, datefmt: str, log_colors: Mapping[str, str]) -> None:
"""Constructor.
Args:
@@ -759,8 +750,8 @@ class HTMLFormatter(logging.Formatter):
log_colors: The colors to use for logging levels.
"""
super().__init__(fmt, datefmt)
- self._log_colors = log_colors # type: typing.Mapping[str, str]
- self._colordict = {} # type: typing.Mapping[str, str]
+ self._log_colors: Mapping[str, str] = log_colors
+ self._colordict: Mapping[str, str] = {}
# We could solve this nicer by using CSS, but for this simple case this
# works.
for color in COLORS:
diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py
index 2754d87e7..1009f1e98 100644
--- a/qutebrowser/utils/message.py
+++ b/qutebrowser/utils/message.py
@@ -23,11 +23,11 @@
"""Message singleton so we don't have to define unneeded signals."""
import traceback
-import typing
+from typing import Any, Callable, Iterable, List, Tuple, Union
-from PyQt5.QtCore import pyqtSignal, QObject
+from PyQt5.QtCore import pyqtSignal, pyqtBoundSignal, QObject
-from qutebrowser.utils import usertypes, log, utils
+from qutebrowser.utils import usertypes, log
def _log_stack(typ: str, stack: str) -> None:
@@ -86,8 +86,8 @@ def info(message: str, *, replace: bool = False) -> None:
def _build_question(title: str,
text: str = None, *,
mode: usertypes.PromptMode,
- default: typing.Union[None, bool, str] = None,
- abort_on: typing.Iterable[pyqtSignal] = (),
+ default: Union[None, bool, str] = None,
+ abort_on: Iterable[pyqtBoundSignal] = (),
url: str = None,
option: bool = None) -> usertypes.Question:
"""Common function for ask/ask_async."""
@@ -110,7 +110,7 @@ def _build_question(title: str,
return question
-def ask(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+def ask(*args: Any, **kwargs: Any) -> Any:
"""Ask a modular question in the statusbar (blocking).
Args:
@@ -134,8 +134,8 @@ def ask(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
def ask_async(title: str,
mode: usertypes.PromptMode,
- handler: typing.Callable[[typing.Any], None],
- **kwargs: typing.Any) -> None:
+ handler: Callable[[Any], None],
+ **kwargs: Any) -> None:
"""Ask an async question in the statusbar.
Args:
@@ -151,13 +151,13 @@ def ask_async(title: str,
global_bridge.ask(question, blocking=False)
-_ActionType = typing.Callable[[], typing.Any]
+_ActionType = Callable[[], Any]
def confirm_async(*, yes_action: _ActionType,
no_action: _ActionType = None,
cancel_action: _ActionType = None,
- **kwargs: typing.Any) -> usertypes.Question:
+ **kwargs: Any) -> usertypes.Question:
"""Ask a yes/no question to the user and execute the given actions.
Args:
@@ -219,8 +219,7 @@ class GlobalMessageBridge(QObject):
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
self._connected = False
- self._cache = [
- ] # type: typing.List[typing.Tuple[usertypes.MessageLevel, str, bool]]
+ self._cache: List[Tuple[usertypes.MessageLevel, str, bool]] = []
def ask(self, question: usertypes.Question,
blocking: bool, *,
@@ -259,42 +258,4 @@ class GlobalMessageBridge(QObject):
self._cache = []
-class MessageBridge(QObject):
-
- """Bridge for messages to be shown in the statusbar.
-
- Signals:
- s_set_text: Set a persistent text in the statusbar.
- arg: The text to set.
- s_maybe_reset_text: Reset the text if it hasn't been changed yet.
- arg: The expected text.
- """
-
- s_set_text = pyqtSignal(str)
- s_maybe_reset_text = pyqtSignal(str)
-
- def __repr__(self) -> str:
- return utils.get_repr(self)
-
- def set_text(self, text: str, *, log_stack: bool = False) -> None:
- """Set the normal text of the statusbar.
-
- Args:
- text: The text to set.
- log_stack: ignored
- """
- text = str(text)
- log.message.debug(text)
- self.s_set_text.emit(text)
-
- def maybe_reset_text(self, text: str, *, log_stack: bool = False) -> None:
- """Reset the text in the statusbar if it matches an expected text.
-
- Args:
- text: The expected text.
- log_stack: ignored
- """
- self.s_maybe_reset_text.emit(str(text))
-
-
global_bridge = GlobalMessageBridge()
diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py
index 015334990..c74cb9873 100644
--- a/qutebrowser/utils/objreg.py
+++ b/qutebrowser/utils/objreg.py
@@ -22,18 +22,19 @@
import collections
import functools
-import typing
+from typing import (TYPE_CHECKING, Any, Callable, MutableMapping, MutableSequence,
+ Optional, Sequence, Union)
from PyQt5.QtCore import QObject, QTimer
from PyQt5.QtWidgets import QApplication
-from PyQt5.QtWidgets import QWidget # pylint: disable=unused-import
+from PyQt5.QtWidgets import QWidget
-from qutebrowser.utils import log, usertypes
-if typing.TYPE_CHECKING:
+from qutebrowser.utils import log, usertypes, utils
+if TYPE_CHECKING:
from qutebrowser.mainwindow import mainwindow
-_WindowTab = typing.Union[str, int, None]
+_WindowTab = Union[str, int, None]
class RegistryUnavailableError(Exception):
@@ -51,7 +52,7 @@ class CommandOnlyError(Exception):
"""Raised when an object is requested which is used for commands only."""
-_IndexType = typing.Union[str, int]
+_IndexType = Union[str, int]
class ObjectRegistry(collections.UserDict):
@@ -67,11 +68,10 @@ class ObjectRegistry(collections.UserDict):
def __init__(self) -> None:
super().__init__()
- self._partial_objs = {
- } # type: typing.MutableMapping[_IndexType, typing.Callable[[], None]]
- self.command_only = [] # type: typing.MutableSequence[str]
+ self._partial_objs: MutableMapping[_IndexType, Callable[[], None]] = {}
+ self.command_only: MutableSequence[str] = []
- def __setitem__(self, name: _IndexType, obj: typing.Any) -> None:
+ def __setitem__(self, name: _IndexType, obj: Any) -> None:
"""Register an object in the object registry.
Sets a slot to remove QObjects when they are destroyed.
@@ -139,7 +139,7 @@ class ObjectRegistry(collections.UserDict):
except KeyError:
pass
- def dump_objects(self) -> typing.Sequence[str]:
+ def dump_objects(self) -> Sequence[str]:
"""Dump all objects as a list of strings."""
lines = []
for name, obj in self.data.items():
@@ -166,7 +166,7 @@ def _get_tab_registry(win_id: _WindowTab,
if tab_id is None:
raise ValueError("Got tab_id None (win_id {})".format(win_id))
if tab_id == 'current' and win_id is None:
- window = QApplication.activeWindow() # type: typing.Optional[QWidget]
+ window: Optional[QWidget] = QApplication.activeWindow()
if window is None or not hasattr(window, 'win_id'):
raise RegistryUnavailableError('tab')
win_id = window.win_id
@@ -192,7 +192,7 @@ def _get_window_registry(window: _WindowTab) -> ObjectRegistry:
raise TypeError("window is None with scope window!")
try:
if window == 'current':
- win = QApplication.activeWindow() # type: typing.Optional[QWidget]
+ win: Optional[QWidget] = QApplication.activeWindow()
elif window == 'last-focused':
win = last_focused_window()
else:
@@ -228,11 +228,11 @@ def _get_registry(scope: str,
def get(name: str,
- default: typing.Any = usertypes.UNSET,
+ default: Any = usertypes.UNSET,
scope: str = 'global',
window: _WindowTab = None,
tab: _WindowTab = None,
- from_command: bool = False) -> typing.Any:
+ from_command: bool = False) -> Any:
"""Helper function to get an object.
Args:
@@ -253,7 +253,7 @@ def get(name: str,
def register(name: str,
- obj: typing.Any,
+ obj: Any,
update: bool = False,
scope: str = None,
registry: ObjectRegistry = None,
@@ -296,7 +296,7 @@ def delete(name: str,
del reg[name]
-def dump_objects() -> typing.Sequence[str]:
+def dump_objects() -> Sequence[str]:
"""Get all registered objects in all registries as a string."""
blocks = []
lines = []
@@ -321,22 +321,50 @@ def dump_objects() -> typing.Sequence[str]:
def last_visible_window() -> 'mainwindow.MainWindow':
"""Get the last visible window, or the last focused window if none."""
try:
- return get('last-visible-main-window')
+ window = get('last-visible-main-window')
except KeyError:
return last_focused_window()
+ if window.tabbed_browser.is_shutting_down:
+ return last_focused_window()
+ return window
def last_focused_window() -> 'mainwindow.MainWindow':
"""Get the last focused window, or the last window if none."""
try:
- return get('last-focused-main-window')
+ window = get('last-focused-main-window')
except KeyError:
- return window_by_index(-1)
+ return last_opened_window()
+ if window.tabbed_browser.is_shutting_down:
+ return last_opened_window()
+ return window
-def window_by_index(idx: int) -> 'mainwindow.MainWindow':
+def _window_by_index(idx: int) -> 'mainwindow.MainWindow':
"""Get the Nth opened window object."""
if not window_registry:
raise NoWindow()
key = sorted(window_registry)[idx]
return window_registry[key]
+
+
+def last_opened_window() -> 'mainwindow.MainWindow':
+ """Get the last opened window object."""
+ if not window_registry:
+ raise NoWindow()
+ for idx in range(-1, -(len(window_registry)+1), -1):
+ window = _window_by_index(idx)
+ if not window.tabbed_browser.is_shutting_down:
+ return window
+ raise utils.Unreachable()
+
+
+def first_opened_window() -> 'mainwindow.MainWindow':
+ """Get the first opened window object."""
+ if not window_registry:
+ raise NoWindow()
+ for idx in range(0, len(window_registry)+1):
+ window = _window_by_index(idx)
+ if not window.tabbed_browser.is_shutting_down:
+ return window
+ raise utils.Unreachable()
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index e853b38f8..cd6ea2b32 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -24,16 +24,15 @@ Module attributes:
value.
MINVALS: A dictionary of C/Qt types (as string) mapped to their minimum
value.
- MAX_WORLD_ID: The highest world ID allowed in this version of QtWebEngine.
+ MAX_WORLD_ID: The highest world ID allowed by QtWebEngine.
"""
import io
import operator
import contextlib
-import typing
+from typing import TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, Tuple, cast
-import pkg_resources
from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR,
PYQT_VERSION_STR, QObject, QUrl)
@@ -43,9 +42,12 @@ try:
from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover
qWebKitVersion = None # type: ignore[assignment] # noqa: N816
+if TYPE_CHECKING:
+ from PyQt5.QtWebKit import QWebHistory
+ from PyQt5.QtWebEngineWidgets import QWebEngineHistory
from qutebrowser.misc import objects
-from qutebrowser.utils import usertypes
+from qutebrowser.utils import usertypes, utils
MAXVALS = {
@@ -71,7 +73,7 @@ class QtOSError(OSError):
if msg is None:
msg = dev.errorString()
- self.qt_errno = None # type: typing.Optional[QFileDevice.FileError]
+ self.qt_errno: Optional[QFileDevice.FileError] = None
if isinstance(dev, QFileDevice):
msg = self._init_filedev(dev, msg)
@@ -97,27 +99,26 @@ def version_check(version: str,
if compiled and exact:
raise ValueError("Can't use compiled=True with exact=True!")
- parsed = pkg_resources.parse_version(version)
+ parsed = utils.parse_version(version)
op = operator.eq if exact else operator.ge
- result = op(pkg_resources.parse_version(qVersion()), parsed)
+ result = op(utils.parse_version(qVersion()), parsed)
if compiled and result:
# qVersion() ==/>= parsed, now check if QT_VERSION_STR ==/>= parsed.
- result = op(pkg_resources.parse_version(QT_VERSION_STR), parsed)
+ result = op(utils.parse_version(QT_VERSION_STR), parsed)
if compiled and result:
# Finally, check PYQT_VERSION_STR as well.
- result = op(pkg_resources.parse_version(PYQT_VERSION_STR), parsed)
+ result = op(utils.parse_version(PYQT_VERSION_STR), parsed)
return result
-# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-69904
-MAX_WORLD_ID = 256 if version_check('5.11.2') else 11
+MAX_WORLD_ID = 256
def is_new_qtwebkit() -> bool:
"""Check if the given version is a new QtWebKit."""
assert qWebKitVersion is not None
- return (pkg_resources.parse_version(qWebKitVersion()) >
- pkg_resources.parse_version('538.1'))
+ return (utils.parse_version(qWebKitVersion()) >
+ utils.parse_version('538.1'))
def is_single_process() -> bool:
@@ -155,16 +156,15 @@ def check_overflow(arg: int, ctype: str, fatal: bool = True) -> int:
return arg
-if typing.TYPE_CHECKING:
- class Validatable(typing.Protocol):
+class Validatable(utils.Protocol):
- """An object with an isValid() method (e.g. QUrl)."""
+ """An object with an isValid() method (e.g. QUrl)."""
- def isValid(self) -> bool:
- ...
+ def isValid(self) -> bool:
+ ...
-def ensure_valid(obj: 'Validatable') -> None:
+def ensure_valid(obj: Validatable) -> None:
"""Ensure a Qt object with an .isValid() method is valid."""
if not obj.isValid():
raise QtValueError(obj)
@@ -184,7 +184,13 @@ def check_qdatastream(stream: QDataStream) -> None:
raise OSError(status_to_str[stream.status()])
-_QtSerializableType = typing.Union[QObject, QByteArray, QUrl]
+_QtSerializableType = Union[
+ QObject,
+ QByteArray,
+ QUrl,
+ 'QWebEngineHistory',
+ 'QWebHistory'
+]
def serialize(obj: _QtSerializableType) -> QByteArray:
@@ -222,7 +228,7 @@ def savefile_open(
filename: str,
binary: bool = False,
encoding: str = 'utf-8'
-) -> typing.Iterator[typing.IO]:
+) -> Iterator[IO]:
"""Context manager to easily use a QSaveFile."""
f = QSaveFile(filename)
cancelled = False
@@ -231,10 +237,10 @@ def savefile_open(
if not open_ok:
raise QtOSError(f)
- dev = typing.cast(typing.BinaryIO, PyQIODevice(f))
+ dev = cast(BinaryIO, PyQIODevice(f))
if binary:
- new_f = dev # type: typing.IO
+ new_f: IO = dev
else:
new_f = io.TextIOWrapper(dev, encoding=encoding)
@@ -352,21 +358,21 @@ class PyQIODevice(io.BufferedIOBase):
def readable(self) -> bool:
return self.dev.isReadable()
- def readline(self, size: int = -1) -> bytes:
+ def readline(self, size: Optional[int] = -1) -> bytes:
self._check_open()
self._check_readable()
- if size < 0:
+ if size is None or size < 0:
qt_size = 0 # no maximum size
elif size == 0:
return b''
else:
qt_size = size + 1 # Qt also counts the NUL byte
- buf = None # type: typing.Union[QByteArray, bytes, None]
+ buf: Union[QByteArray, bytes, None] = None
if self.dev.canReadLine():
buf = self.dev.readLine(qt_size)
- elif size < 0:
+ elif size is None or size < 0:
buf = self.dev.readAll()
else:
buf = self.dev.read(size)
@@ -392,7 +398,10 @@ class PyQIODevice(io.BufferedIOBase):
def writable(self) -> bool:
return self.dev.isWritable()
- def write(self, data: typing.Union[bytes, bytearray]) -> int:
+ def write( # type: ignore[override]
+ self,
+ data: Union[bytes, bytearray]
+ ) -> int:
self._check_open()
self._check_writable()
num = self.dev.write(data)
@@ -400,11 +409,11 @@ class PyQIODevice(io.BufferedIOBase):
raise QtOSError(self.dev)
return num
- def read(self, size: typing.Optional[int] = None) -> bytes:
+ def read(self, size: Optional[int] = None) -> bytes:
self._check_open()
self._check_readable()
- buf = None # type: typing.Union[QByteArray, bytes, None]
+ buf: Union[QByteArray, bytes, None] = None
if size in [None, -1]:
buf = self.dev.readAll()
else:
@@ -426,7 +435,7 @@ class QtValueError(ValueError):
"""Exception which gets raised by ensure_valid."""
- def __init__(self, obj: 'Validatable') -> None:
+ def __init__(self, obj: Validatable) -> None:
try:
self.reason = obj.errorString() # type: ignore[attr-defined]
except AttributeError:
@@ -451,7 +460,7 @@ class EventLoop(QEventLoop):
def exec_(
self,
flags: QEventLoop.ProcessEventsFlags =
- typing.cast(QEventLoop.ProcessEventsFlags, QEventLoop.AllEvents)
+ cast(QEventLoop.ProcessEventsFlags, QEventLoop.AllEvents)
) -> int:
"""Override exec_ to raise an exception when re-running."""
if self._executing:
@@ -460,3 +469,78 @@ class EventLoop(QEventLoop):
status = super().exec_(flags)
self._executing = False
return status
+
+
+def _get_color_percentage(x1: int, y1: int, z1: int, a1: int,
+ x2: int, y2: int, z2: int, a2: int,
+ percent: int) -> Tuple[int, int, int, int]:
+ """Get a color which is percent% interpolated between start and end.
+
+ Args:
+ x1, y1, z1, a1 : Start color components (R, G, B, A / H, S, V, A / H, S, L, A)
+ x2, y2, z2, a2 : End color components (R, G, B, A / H, S, V, A / H, S, L, A)
+ percent: Percentage to interpolate, 0-100.
+ 0: Start color will be returned.
+ 100: End color will be returned.
+
+ Return:
+ A (x, y, z, alpha) tuple with the interpolated color components.
+ """
+ if not 0 <= percent <= 100:
+ raise ValueError("percent needs to be between 0 and 100!")
+ x = round(x1 + (x2 - x1) * percent / 100)
+ y = round(y1 + (y2 - y1) * percent / 100)
+ z = round(z1 + (z2 - z1) * percent / 100)
+ a = round(a1 + (a2 - a1) * percent / 100)
+ return (x, y, z, a)
+
+
+def interpolate_color(
+ start: QColor,
+ end: QColor,
+ percent: int,
+ colorspace: Optional[QColor.Spec] = QColor.Rgb
+) -> QColor:
+ """Get an interpolated color value.
+
+ Args:
+ start: The start color.
+ end: The end color.
+ percent: Which value to get (0 - 100)
+ colorspace: The desired interpolation color system,
+ QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum)
+ If None, start is used except when percent is 100.
+
+ Return:
+ The interpolated QColor, with the same spec as the given start color.
+ """
+ ensure_valid(start)
+ ensure_valid(end)
+
+ if colorspace is None:
+ if percent == 100:
+ return QColor(*end.getRgb())
+ else:
+ return QColor(*start.getRgb())
+
+ out = QColor()
+ if colorspace == QColor.Rgb:
+ r1, g1, b1, a1 = start.getRgb()
+ r2, g2, b2, a2 = end.getRgb()
+ components = _get_color_percentage(r1, g1, b1, a1, r2, g2, b2, a2, percent)
+ out.setRgb(*components)
+ elif colorspace == QColor.Hsv:
+ h1, s1, v1, a1 = start.getHsv()
+ h2, s2, v2, a2 = end.getHsv()
+ components = _get_color_percentage(h1, s1, v1, a1, h2, s2, v2, a2, percent)
+ out.setHsv(*components)
+ elif colorspace == QColor.Hsl:
+ h1, s1, l1, a1 = start.getHsl()
+ h2, s2, l2, a2 = end.getHsl()
+ components = _get_color_percentage(h1, s1, l1, a1, h2, s2, l2, a2, percent)
+ out.setHsl(*components)
+ else:
+ raise ValueError("Invalid colorspace!")
+ out = out.convertTo(start.spec())
+ ensure_valid(out)
+ return out
diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py
index 8e5a91c30..ff4afc83c 100644
--- a/qutebrowser/utils/standarddir.py
+++ b/qutebrowser/utils/standarddir.py
@@ -22,16 +22,15 @@
import os
import os.path
import sys
-import shutil
import contextlib
import enum
import argparse
-import typing
+from typing import Iterator, Optional
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
-from qutebrowser.utils import log, debug, message, utils
+from qutebrowser.utils import log, debug, utils
# The cached locations
_locations = {}
@@ -41,14 +40,14 @@ class _Location(enum.Enum):
"""A key for _locations."""
- config = 1
- auto_config = 2
- data = 3
- system_data = 4
- cache = 5
- download = 6
- runtime = 7
- config_py = 8
+ config = enum.auto()
+ auto_config = enum.auto()
+ data = enum.auto()
+ system_data = enum.auto()
+ cache = enum.auto()
+ download = enum.auto()
+ runtime = enum.auto()
+ config_py = enum.auto()
APPNAME = 'qutebrowser'
@@ -60,7 +59,7 @@ class EmptyValueError(Exception):
@contextlib.contextmanager
-def _unset_organization() -> typing.Iterator[None]:
+def _unset_organization() -> Iterator[None]:
"""Temporarily unset QApplication.organizationName().
This is primarily needed in config.py.
@@ -76,7 +75,7 @@ def _unset_organization() -> typing.Iterator[None]:
qapp.setOrganizationName(orgname)
-def _init_config(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_config(args: Optional[argparse.Namespace]) -> None:
"""Initialize the location for configs."""
typ = QStandardPaths.ConfigLocation
path = _from_args(typ, args)
@@ -127,13 +126,13 @@ def config_py() -> str:
return _locations[_Location.config_py]
-def _init_data(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_data(args: Optional[argparse.Namespace]) -> None:
"""Initialize the location for data."""
- typ = QStandardPaths.DataLocation
+ typ = QStandardPaths.AppDataLocation
path = _from_args(typ, args)
if path is None:
if utils.is_windows:
- app_data_path = _writable_location(QStandardPaths.AppDataLocation)
+ app_data_path = _writable_location(typ) # same location as config
path = os.path.join(app_data_path, 'data')
elif sys.platform.startswith('haiku'):
# HaikuOS returns an empty value for AppDataLocation
@@ -167,14 +166,14 @@ def data(system: bool = False) -> str:
return _locations[_Location.data]
-def _init_cache(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_cache(args: Optional[argparse.Namespace]) -> None:
"""Initialize the location for the cache."""
typ = QStandardPaths.CacheLocation
path = _from_args(typ, args)
if path is None:
if utils.is_windows:
# Local, not Roaming!
- data_path = _writable_location(QStandardPaths.DataLocation)
+ data_path = _writable_location(QStandardPaths.AppLocalDataLocation)
path = os.path.join(data_path, 'cache')
else:
path = _writable_location(typ)
@@ -187,7 +186,7 @@ def cache() -> str:
return _locations[_Location.cache]
-def _init_download(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_download(args: Optional[argparse.Namespace]) -> None:
"""Initialize the location for downloads.
Note this is only the default directory as found by Qt.
@@ -204,7 +203,7 @@ def download() -> str:
return _locations[_Location.download]
-def _init_runtime(args: typing.Optional[argparse.Namespace]) -> None:
+def _init_runtime(args: Optional[argparse.Namespace]) -> None:
"""Initialize location for runtime data."""
if utils.is_mac or utils.is_windows:
# RuntimeLocation is a weird path on macOS and Windows.
@@ -251,11 +250,10 @@ def _writable_location(typ: QStandardPaths.StandardLocation) -> str:
# Types we are sure we handle correctly below.
assert typ in [
- QStandardPaths.ConfigLocation, QStandardPaths.DataLocation,
+ QStandardPaths.ConfigLocation, QStandardPaths.AppLocalDataLocation,
QStandardPaths.CacheLocation, QStandardPaths.DownloadLocation,
QStandardPaths.RuntimeLocation, QStandardPaths.TempLocation,
- # FIXME old Qt
- getattr(QStandardPaths, 'AppDataLocation', object())], typ_str
+ QStandardPaths.AppDataLocation], typ_str
with _unset_organization():
path = QStandardPaths.writableLocation(typ)
@@ -279,8 +277,8 @@ def _writable_location(typ: QStandardPaths.StandardLocation) -> str:
def _from_args(
typ: QStandardPaths.StandardLocation,
- args: typing.Optional[argparse.Namespace]
-) -> typing.Optional[str]:
+ args: Optional[argparse.Namespace]
+) -> Optional[str]:
"""Get the standard directory from an argparse namespace.
Return:
@@ -288,7 +286,8 @@ def _from_args(
"""
basedir_suffix = {
QStandardPaths.ConfigLocation: 'config',
- QStandardPaths.DataLocation: 'data',
+ QStandardPaths.AppDataLocation: 'data',
+ QStandardPaths.AppLocalDataLocation: 'data',
QStandardPaths.CacheLocation: 'cache',
QStandardPaths.DownloadLocation: 'download',
QStandardPaths.RuntimeLocation: 'runtime',
@@ -332,7 +331,7 @@ def _init_dirs(args: argparse.Namespace = None) -> None:
_init_runtime(args)
-def init(args: typing.Optional[argparse.Namespace]) -> None:
+def init(args: Optional[argparse.Namespace]) -> None:
"""Initialize all standard dirs."""
if args is not None:
# args can be None during tests
@@ -340,44 +339,6 @@ def init(args: typing.Optional[argparse.Namespace]) -> None:
_init_dirs(args)
_init_cachedir_tag()
- if args is not None and getattr(args, 'basedir', None) is None:
- if utils.is_mac: # pragma: no cover
- _move_macos()
- elif utils.is_windows: # pragma: no cover
- _move_windows()
-
-
-def _move_macos() -> None:
- """Move most config files to new location on macOS."""
- old_config = config(auto=True) # ~/Library/Preferences/qutebrowser
- new_config = config() # ~/.qutebrowser
- for f in os.listdir(old_config):
- if f not in ['qsettings', 'autoconfig.yml']:
- _move_data(os.path.join(old_config, f),
- os.path.join(new_config, f))
-
-
-def _move_windows() -> None:
- """Move the whole qutebrowser directory from Local to Roaming AppData."""
- # %APPDATA%\Local\qutebrowser
- old_appdata_dir = _writable_location(QStandardPaths.DataLocation)
- # %APPDATA%\Roaming\qutebrowser
- new_appdata_dir = _writable_location(QStandardPaths.AppDataLocation)
-
- # data subfolder
- old_data = os.path.join(old_appdata_dir, 'data')
- new_data = os.path.join(new_appdata_dir, 'data')
- ok = _move_data(old_data, new_data)
- if not ok: # pragma: no cover
- return
-
- # config files
- new_config_dir = os.path.join(new_appdata_dir, 'config')
- _create(new_config_dir)
- for f in os.listdir(old_appdata_dir):
- if f != 'cache':
- _move_data(os.path.join(old_appdata_dir, f),
- os.path.join(new_config_dir, f))
def _init_cachedir_tag() -> None:
@@ -397,33 +358,3 @@ def _init_cachedir_tag() -> None:
"cachedir/\n")
except OSError:
log.init.exception("Failed to create CACHEDIR.TAG")
-
-
-def _move_data(old: str, new: str) -> bool:
- """Migrate data from an old to a new directory.
-
- If the old directory does not exist, the migration is skipped.
- If the new directory already exists, an error is shown.
-
- Return: True if moving succeeded, False otherwise.
- """
- if not os.path.exists(old):
- return False
-
- log.init.debug("Migrating data from {} to {}".format(old, new))
-
- if os.path.exists(new):
- if not os.path.isdir(new) or os.listdir(new):
- message.error("Failed to move data from {} as {} is non-empty!"
- .format(old, new))
- return False
- os.rmdir(new)
-
- try:
- shutil.move(old, new)
- except OSError as e:
- message.error("Failed to move data from {} to {}: {}".format(
- old, new, e))
- return False
-
- return True
diff --git a/qutebrowser/utils/urlmatch.py b/qutebrowser/utils/urlmatch.py
index 503436ef8..e4e4984db 100644
--- a/qutebrowser/utils/urlmatch.py
+++ b/qutebrowser/utils/urlmatch.py
@@ -25,14 +25,14 @@ https://cs.chromium.org/chromium/src/extensions/common/url_pattern.cc
https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h
Based on the following commit in Chromium:
-https://chromium.googlesource.com/chromium/src/+/757854e199e159523e7789de5cb2f6ba49b79b63
-(February 4 2020, newest commit as per July 1st 2020)
+https://chromium.googlesource.com/chromium/src/+/6f4a6681eae01c2036336c18b06303e16a304a7c
+(October 10 2020, newest commit as per October 28th 2020)
"""
import ipaddress
import fnmatch
-import typing
import urllib.parse
+from typing import Any, Optional, Tuple
from PyQt5.QtCore import QUrl
@@ -73,11 +73,11 @@ class UrlPattern:
# Make sure all attributes are initialized if we exit early.
self._pattern = pattern
self._match_all = False
- self._match_subdomains = False # type: bool
- self._scheme = None # type: typing.Optional[str]
- self.host = None # type: typing.Optional[str]
- self._path = None # type: typing.Optional[str]
- self._port = None # type: typing.Optional[int]
+ self._match_subdomains: bool = False
+ self._scheme: Optional[str] = None
+ self.host: Optional[str] = None
+ self._path: Optional[str] = None
+ self._port: Optional[int] = None
# > The special pattern <all_urls> matches any URL that starts with a
# > permitted scheme.
@@ -104,7 +104,7 @@ class UrlPattern:
self._init_path(parsed)
self._init_port(parsed)
- def _to_tuple(self) -> typing.Tuple:
+ def _to_tuple(self) -> Tuple:
"""Get a pattern with information used for __eq__/__hash__."""
return (self._match_all, self._match_subdomains, self._scheme,
self.host, self._path, self._port)
@@ -112,7 +112,7 @@ class UrlPattern:
def __hash__(self) -> int:
return hash(self._to_tuple())
- def __eq__(self, other: typing.Any) -> bool:
+ def __eq__(self, other: Any) -> bool:
if not isinstance(other, UrlPattern):
return NotImplemented
return self._to_tuple() == other._to_tuple()
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index a691b2cbc..41d20e734 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -25,9 +25,9 @@ import os.path
import ipaddress
import posixpath
import urllib.parse
-import typing
+from typing import Optional, Tuple, Union
-from PyQt5.QtCore import QUrl, QUrlQuery
+from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy
from qutebrowser.api import cmdutils
@@ -72,8 +72,7 @@ class InvalidUrlError(Error):
super().__init__(self.msg)
-def _parse_search_term(s: str) -> typing.Tuple[typing.Optional[str],
- typing.Optional[str]]:
+def _parse_search_term(s: str) -> Tuple[Optional[str], Optional[str]]:
"""Get a search engine name and search term from a string.
Args:
@@ -89,8 +88,8 @@ def _parse_search_term(s: str) -> typing.Tuple[typing.Optional[str],
if len(split) == 2:
if split[0] in config.val.url.searchengines:
- engine = split[0] # type: typing.Optional[str]
- term = split[1] # type: typing.Optional[str]
+ engine: Optional[str] = split[0]
+ term: Optional[str] = split[1]
else:
engine = None
term = s
@@ -127,9 +126,9 @@ def _get_search_url(txt: str) -> QUrl:
unquoted=term,
quoted=quoted_term,
semiquoted=semiquoted_term)
- url = qurl_from_user_input(evaluated)
+ url = QUrl.fromUserInput(evaluated)
else:
- url = qurl_from_user_input(config.val.url.searchengines[engine])
+ url = QUrl.fromUserInput(config.val.url.searchengines[engine])
url.setPath(None) # type: ignore[arg-type]
url.setFragment(None) # type: ignore[arg-type]
url.setQuery(None) # type: ignore[call-overload]
@@ -146,7 +145,7 @@ def _is_url_naive(urlstr: str) -> bool:
Return:
True if the URL really is a URL, False otherwise.
"""
- url = qurl_from_user_input(urlstr)
+ url = QUrl.fromUserInput(urlstr)
assert url.isValid()
host = url.host()
@@ -171,7 +170,7 @@ def _is_url_dns(urlstr: str) -> bool:
Return:
True if the URL really is a URL, False otherwise.
"""
- url = qurl_from_user_input(urlstr)
+ url = QUrl.fromUserInput(urlstr)
assert url.isValid()
if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and
@@ -220,10 +219,10 @@ def fuzzy_url(urlstr: str,
try:
url = _get_search_url(urlstr)
except ValueError: # invalid search engine
- url = qurl_from_user_input(urlstr)
+ url = QUrl.fromUserInput(urlstr)
else: # probably an address
log.url.debug("URL is a fuzzy address")
- url = qurl_from_user_input(urlstr)
+ url = QUrl.fromUserInput(urlstr)
log.url.debug("Converting fuzzy term {!r} to URL -> {}".format(
urlstr, url.toDisplayString()))
ensure_valid(url)
@@ -273,7 +272,7 @@ def is_url(urlstr: str) -> bool:
urlstr = urlstr.strip()
qurl = QUrl(urlstr)
- qurl_userinput = qurl_from_user_input(urlstr)
+ qurl_userinput = QUrl.fromUserInput(urlstr)
if autosearch == 'never':
# no autosearch, so everything is a URL unless it has an explicit
@@ -307,7 +306,7 @@ def is_url(urlstr: str) -> bool:
url = True
elif autosearch == 'dns':
log.url.debug("Checking via DNS check")
- # We want to use qurl_from_user_input here, as the user might enter
+ # We want to use QUrl.fromUserInput here, as the user might enter
# "foo.de" and that should be treated as URL here.
url = ' ' not in qurl_userinput.userName() and _is_url_dns(urlstr)
elif autosearch == 'naive':
@@ -319,42 +318,6 @@ def is_url(urlstr: str) -> bool:
return url
-def qurl_from_user_input(urlstr: str) -> QUrl:
- """Get a QUrl based on a user input. Additionally handles IPv6 addresses.
-
- QUrl.fromUserInput handles something like '::1' as a file URL instead of an
- IPv6, so we first try to handle it as a valid IPv6, and if that fails we
- use QUrl.fromUserInput.
-
- WORKAROUND - https://bugreports.qt.io/browse/QTBUG-41089
- FIXME - Maybe https://codereview.qt-project.org/#/c/93851/ has a better way
- to solve this?
- https://github.com/qutebrowser/qutebrowser/issues/109
-
- Args:
- urlstr: The URL as string.
-
- Return:
- The converted QUrl.
- """
- # First we try very liberally to separate something like an IPv6 from the
- # rest (e.g. path info or parameters)
- match = re.fullmatch(r'\[?([0-9a-fA-F:.]+)\]?(.*)', urlstr.strip())
- if match:
- ipstr, rest = match.groups()
- else:
- ipstr = urlstr.strip()
- rest = ''
- # Then we try to parse it as an IPv6, and if we fail use
- # QUrl.fromUserInput.
- try:
- ipaddress.IPv6Address(ipstr)
- except ipaddress.AddressValueError:
- return QUrl.fromUserInput(urlstr)
- else:
- return QUrl('http://[{}]{}'.format(ipstr, rest))
-
-
def ensure_valid(url: QUrl) -> None:
if not url.isValid():
raise InvalidUrlError(url)
@@ -385,7 +348,7 @@ def raise_cmdexc_if_invalid(url: QUrl) -> None:
def get_path_if_valid(pathstr: str,
cwd: str = None,
relative: bool = False,
- check_exists: bool = False) -> typing.Optional[str]:
+ check_exists: bool = False) -> Optional[str]:
"""Check if path is a valid path.
Args:
@@ -403,7 +366,7 @@ def get_path_if_valid(pathstr: str,
expanded = os.path.expanduser(pathstr)
if os.path.isabs(expanded):
- path = expanded # type: typing.Optional[str]
+ path: Optional[str] = expanded
elif relative and cwd:
path = os.path.join(cwd, expanded)
elif relative:
@@ -430,7 +393,7 @@ def get_path_if_valid(pathstr: str,
return path
-def filename_from_url(url: QUrl) -> typing.Optional[str]:
+def filename_from_url(url: QUrl) -> Optional[str]:
"""Get a suitable filename from a URL.
Args:
@@ -450,7 +413,7 @@ def filename_from_url(url: QUrl) -> typing.Optional[str]:
return None
-HostTupleType = typing.Tuple[str, str, int]
+HostTupleType = Tuple[str, str, int]
def host_tuple(url: QUrl) -> HostTupleType:
@@ -504,12 +467,19 @@ def same_domain(url1: QUrl, url2: QUrl) -> bool:
For example example.com and www.example.com are considered the same. but
example.co.uk and test.co.uk are not.
+ If the URL's schemes or ports are different, they are always treated as not equal.
+
Return:
True if the domains are the same, False otherwise.
"""
ensure_valid(url1)
ensure_valid(url2)
+ if url1.scheme() != url2.scheme():
+ return False
+ if url1.port() != url2.port():
+ return False
+
suffix1 = url1.topLevelDomain()
suffix2 = url2.topLevelDomain()
if not suffix1:
@@ -562,9 +532,7 @@ def safe_display_string(qurl: QUrl) -> str:
ensure_valid(qurl)
host = qurl.host(QUrl.FullyEncoded)
- if '..' in host: # pragma: no cover
- # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60364
- return '(unparseable URL!) {}'.format(qurl.toDisplayString())
+ assert '..' not in host, qurl # https://bugreports.qt.io/browse/QTBUG-60364
for part in host.split('.'):
url_host = qurl.host(QUrl.FullyDecoded)
@@ -574,18 +542,6 @@ def safe_display_string(qurl: QUrl) -> str:
return qurl.toDisplayString()
-def query_string(qurl: QUrl) -> str:
- """Get a query string for the given URL.
-
- This is a WORKAROUND for:
- https://www.riverbankcomputing.com/pipermail/pyqt/2017-November/039702.html
- """
- try:
- return qurl.query()
- except AttributeError: # pragma: no cover
- return QUrlQuery(qurl).query()
-
-
class InvalidProxyTypeError(Exception):
"""Error raised when proxy_from_url gets an unknown proxy type."""
@@ -594,7 +550,7 @@ class InvalidProxyTypeError(Exception):
super().__init__("Invalid proxy type {}!".format(typ))
-def proxy_from_url(url: QUrl) -> typing.Union[QNetworkProxy, pac.PACFetcher]:
+def proxy_from_url(url: QUrl) -> Union[QNetworkProxy, pac.PACFetcher]:
"""Create a QNetworkProxy from QUrl and a proxy type.
Args:
@@ -644,12 +600,10 @@ def parse_javascript_url(url: QUrl) -> str:
raise Error("URL contains unexpected components: {}"
.format(url.authority()))
- code = url.path(QUrl.FullyDecoded)
- if url.hasQuery():
- code += '?' + url.query(QUrl.FullyDecoded)
- if url.hasFragment():
- code += '#' + url.fragment(QUrl.FullyDecoded)
+ urlstr = url.toString(QUrl.FullyEncoded) # type: ignore[arg-type]
+ urlstr = urllib.parse.unquote(urlstr)
+ code = urlstr[len('javascript:'):]
if not code:
raise Error("Resulted in empty JavaScript code")
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index 0b6f9c219..893dae877 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -21,7 +21,7 @@
import operator
import enum
-import typing
+from typing import Any, Optional, Sequence, TypeVar, Union
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
@@ -30,7 +30,7 @@ from PyQt5.QtCore import QUrl
from qutebrowser.utils import log, qtutils, utils
-_T = typing.TypeVar('_T')
+_T = TypeVar('_T', bound=utils.SupportsLessThan)
class Unset:
@@ -46,7 +46,7 @@ class Unset:
UNSET = Unset()
-class NeighborList(typing.Sequence[_T]):
+class NeighborList(Sequence[_T]):
"""A list of items which saves its current position.
@@ -60,10 +60,15 @@ class NeighborList(typing.Sequence[_T]):
_mode: The current mode.
"""
- Modes = enum.Enum('Modes', ['edge', 'exception'])
+ class Modes(enum.Enum):
- def __init__(self, items: typing.Sequence[_T] = None,
- default: typing.Union[_T, Unset] = UNSET,
+ """Behavior for the 'mode' argument."""
+
+ edge = enum.auto()
+ exception = enum.auto()
+
+ def __init__(self, items: Sequence[_T] = None,
+ default: Union[_T, Unset] = UNSET,
mode: Modes = Modes.exception) -> None:
"""Constructor.
@@ -77,19 +82,19 @@ class NeighborList(typing.Sequence[_T]):
if not isinstance(mode, self.Modes):
raise TypeError("Mode {} is not a Modes member!".format(mode))
if items is None:
- self._items = [] # type: typing.Sequence[_T]
+ self._items: Sequence[_T] = []
else:
self._items = list(items)
self._default = default
if not isinstance(default, Unset):
idx = self._items.index(default)
- self._idx = idx # type: typing.Optional[int]
+ self._idx: Optional[int] = idx
else:
self._idx = None
self._mode = mode
- self.fuzzyval = None # type: typing.Optional[int]
+ self.fuzzyval: Optional[int] = None
def __getitem__(self, key: int) -> _T: # type: ignore[override]
return self._items[key]
@@ -158,7 +163,7 @@ class NeighborList(typing.Sequence[_T]):
return new
@property
- def items(self) -> typing.Sequence[_T]:
+ def items(self) -> Sequence[_T]:
"""Getter for items, which should not be set."""
return self._items
@@ -224,42 +229,48 @@ class NeighborList(typing.Sequence[_T]):
return self.curitem()
-# The mode of a Question.
-PromptMode = enum.Enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
- 'download'])
+class PromptMode(enum.Enum):
+
+ """The mode of a Question."""
+
+ yesno = enum.auto()
+ text = enum.auto()
+ user_pwd = enum.auto()
+ alert = enum.auto()
+ download = enum.auto()
class ClickTarget(enum.Enum):
"""How to open a clicked link."""
- normal = 0 #: Open the link in the current tab
- tab = 1 #: Open the link in a new foreground tab
- tab_bg = 2 #: Open the link in a new background tab
- window = 3 #: Open the link in a new window
- hover = 4 #: Only hover over the link
+ normal = enum.auto() #: Open the link in the current tab
+ tab = enum.auto() #: Open the link in a new foreground tab
+ tab_bg = enum.auto() #: Open the link in a new background tab
+ window = enum.auto() #: Open the link in a new window
+ hover = enum.auto() #: Only hover over the link
class KeyMode(enum.Enum):
"""Key input modes."""
- normal = 1 #: Normal mode (no mode was entered)
- hint = 2 #: Hint mode (showing labels for links)
- command = 3 #: Command mode (after pressing the colon key)
- yesno = 4 #: Yes/No prompts
- prompt = 5 #: Text prompts
- insert = 6 #: Insert mode (passing through most keys)
- passthrough = 7 #: Passthrough mode (passing through all keys)
- caret = 8 #: Caret mode (moving cursor with keys)
- set_mark = 9
- jump_mark = 10
- record_macro = 11
- run_macro = 12
+ normal = enum.auto() #: Normal mode (no mode was entered)
+ hint = enum.auto() #: Hint mode (showing labels for links)
+ command = enum.auto() #: Command mode (after pressing the colon key)
+ yesno = enum.auto() #: Yes/No prompts
+ prompt = enum.auto() #: Text prompts
+ insert = enum.auto() #: Insert mode (passing through most keys)
+ passthrough = enum.auto() #: Passthrough mode (passing through all keys)
+ caret = enum.auto() #: Caret mode (moving cursor with keys)
+ set_mark = enum.auto()
+ jump_mark = enum.auto()
+ record_macro = enum.auto()
+ run_macro = enum.auto()
# 'register' is a bit of an oddball here: It's not really a "real" mode,
# but it's used in the config for common bindings for
# set_mark/jump_mark/record_macro/run_macro.
- register = 13
+ register = enum.auto()
class Exit(enum.IntEnum):
@@ -273,44 +284,76 @@ class Exit(enum.IntEnum):
err_init = 4
-# Load status of a tab
-LoadStatus = enum.Enum('LoadStatus', ['none', 'success', 'success_https',
- 'error', 'warn', 'loading'])
+class LoadStatus(enum.Enum):
+
+ """Load status of a tab."""
+ none = enum.auto()
+ success = enum.auto()
+ success_https = enum.auto()
+ error = enum.auto()
+ warn = enum.auto()
+ loading = enum.auto()
-# Backend of a tab
-Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine'])
+
+class Backend(enum.Enum):
+
+ """The backend being used (usertypes.backend)."""
+
+ QtWebKit = enum.auto()
+ QtWebEngine = enum.auto()
class JsWorld(enum.Enum):
"""World/context to run JavaScript code in."""
- main = 1 #: Same world as the web page's JavaScript.
- application = 2 #: Application world, used by qutebrowser internally.
- user = 3 #: User world, currently not used.
- jseval = 4 #: World used for the jseval-command.
+ main = enum.auto() #: Same world as the web page's JavaScript.
+ application = enum.auto() #: Application world, used by qutebrowser internally.
+ user = enum.auto() #: User world, currently not used.
+ jseval = enum.auto() #: World used for the jseval-command.
+
+
+class JsLogLevel(enum.Enum):
+ """Log level of a JS message.
-# Log level of a JS message. This needs to match up with the keys allowed for
-# the content.javascript.log setting.
-JsLogLevel = enum.Enum('JsLogLevel', ['unknown', 'info', 'warning', 'error'])
+ This needs to match up with the keys allowed for the
+ content.javascript.log setting.
+ """
+
+ unknown = enum.auto()
+ info = enum.auto()
+ warning = enum.auto()
+ error = enum.auto()
+
+
+class MessageLevel(enum.Enum):
+
+ """The level of a message being shown."""
+ error = enum.auto()
+ warning = enum.auto()
+ info = enum.auto()
-MessageLevel = enum.Enum('MessageLevel', ['error', 'warning', 'info'])
+class IgnoreCase(enum.Enum):
-IgnoreCase = enum.Enum('IgnoreCase', ['smart', 'never', 'always'])
+ """Possible values for the 'search.ignore_case' setting."""
+
+ smart = enum.auto()
+ never = enum.auto()
+ always = enum.auto()
class CommandValue(enum.Enum):
"""Special values which are injected when running a command handler."""
- count = 1
- win_id = 2
- cur_tab = 3
- count_tab = 4
+ count = enum.auto()
+ win_id = enum.auto()
+ cur_tab = enum.auto()
+ count_tab = enum.auto()
class Question(QObject):
@@ -360,13 +403,13 @@ class Question(QObject):
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
- self.mode = None # type: typing.Optional[PromptMode]
- self.default = None # type: typing.Union[bool, str, None]
- self.title = None # type: typing.Optional[str]
- self.text = None # type: typing.Optional[str]
- self.url = None # type: typing.Optional[str]
- self.option = None # type: typing.Optional[bool]
- self.answer = None # type: typing.Union[str, bool, None]
+ self.mode: Optional[PromptMode] = None
+ self.default: Union[bool, str, None] = None
+ self.title: Optional[str] = None
+ self.text: Optional[str] = None
+ self.url: Optional[str] = None
+ self.option: Optional[bool] = None
+ self.answer: Union[str, bool, None] = None
self.is_aborted = False
self.interrupted = False
@@ -440,7 +483,7 @@ class AbstractCertificateErrorWrapper:
"""A wrapper over an SSL/certificate error."""
- def __init__(self, error: typing.Any) -> None:
+ def __init__(self, error: Any) -> None:
self._error = error
def __str__(self) -> str:
@@ -458,18 +501,32 @@ class NavigationRequest:
"""A request to navigate to the given URL."""
- Type = enum.Enum('Type', [
- 'link_clicked',
- 'typed', # QtWebEngine only
- 'form_submitted',
- 'form_resubmitted', # QtWebKit only
- 'back_forward',
- 'reloaded',
- 'redirect', # QtWebEngine >= 5.14 only
- 'other'
- ])
-
- url = attr.ib() # type: QUrl
- navigation_type = attr.ib() # type: Type
- is_main_frame = attr.ib() # type: bool
- accepted = attr.ib(default=True) # type: bool
+ class Type(enum.Enum):
+
+ """The type of a request.
+
+ Based on QWebEngineUrlRequestInfo::NavigationType and QWebPage::NavigationType.
+ """
+
+ #: Navigation initiated by clicking a link.
+ link_clicked = 1
+ #: Navigation explicitly initiated by typing a URL (QtWebEngine only).
+ typed = 2
+ #: Navigation submits a form.
+ form_submitted = 3
+ #: An HTML form was submitted a second time (QtWebKit only).
+ form_resubmitted = 4
+ #: Navigation initiated by a history action.
+ back_forward = 5
+ #: Navigation initiated by refreshing the page.
+ reloaded = 6
+ #: Navigation triggered automatically by page content or remote server
+ #: (QtWebEngine >= 5.14 only)
+ redirect = 7
+ #: None of the above.
+ other = 8
+
+ url: QUrl = attr.ib()
+ navigation_type: Type = attr.ib()
+ is_main_frame: bool = attr.ib()
+ accepted: bool = attr.ib(default=True)
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 0bbba9a4f..31ff5bf50 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -31,16 +31,16 @@ import traceback
import functools
import contextlib
import posixpath
-import socket
import shlex
import glob
import mimetypes
-import typing
import ctypes
import ctypes.util
+from typing import (Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union,
+ TYPE_CHECKING, cast)
-from PyQt5.QtCore import QUrl
-from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
+from PyQt5.QtCore import QUrl, QVersionNumber
+from PyQt5.QtGui import QClipboard, QDesktopServices
from PyQt5.QtWidgets import QApplication
import pkg_resources
import yaml
@@ -54,7 +54,7 @@ except ImportError: # pragma: no cover
YAML_C_EXT = False
import qutebrowser
-from qutebrowser.utils import qtutils, log
+from qutebrowser.utils import log
fake_clipboard = None
@@ -67,6 +67,34 @@ is_windows = sys.platform.startswith('win')
is_posix = os.name == 'posix'
+try:
+ # Protocol was added in Python 3.8
+ from typing import Protocol
+except ImportError: # pragma: no cover
+ if not TYPE_CHECKING:
+ class Protocol:
+
+ """Empty stub at runtime."""
+
+
+class SupportsLessThan(Protocol):
+
+ """Protocol for a "comparable" object."""
+
+ def __lt__(self, other: Any) -> bool:
+ ...
+
+
+if TYPE_CHECKING:
+ class VersionNumber(SupportsLessThan, QVersionNumber):
+
+ """WORKAROUND for incorrect PyQt stubs."""
+else:
+ class VersionNumber:
+
+ """We can't inherit from Protocol and QVersionNumber at runtime."""
+
+
class Unreachable(Exception):
"""Raised when there was unreachable code."""
@@ -159,7 +187,7 @@ def preload_resources() -> None:
# FIXME:typing Return value should be bytes/str
-def read_file(filename: str, binary: bool = False) -> typing.Any:
+def read_file(filename: str, binary: bool = False) -> Any:
"""Get the contents of a file contained with qutebrowser.
Args:
@@ -181,7 +209,8 @@ def read_file(filename: str, binary: bool = False) -> typing.Any:
# https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc
fn = os.path.join(os.path.dirname(sys.executable), filename)
if binary:
- with open(fn, 'rb') as f: # type: typing.IO
+ f: IO
+ with open(fn, 'rb') as f:
return f.read()
else:
with open(fn, 'r', encoding='utf-8') as f:
@@ -210,81 +239,10 @@ def resource_filename(filename: str) -> str:
return pkg_resources.resource_filename(qutebrowser.__name__, filename)
-def _get_color_percentage(a_c1: int, a_c2: int, a_c3:
- int, b_c1: int, b_c2: int, b_c3: int,
- percent: int) -> typing.Tuple[int, int, int]:
- """Get a color which is percent% interpolated between start and end.
-
- Args:
- a_c1, a_c2, a_c3: Start color components (R, G, B / H, S, V / H, S, L)
- b_c1, b_c2, b_c3: End color components (R, G, B / H, S, V / H, S, L)
- percent: Percentage to interpolate, 0-100.
- 0: Start color will be returned.
- 100: End color will be returned.
-
- Return:
- A (c1, c2, c3) tuple with the interpolated color components.
- """
- if not 0 <= percent <= 100:
- raise ValueError("percent needs to be between 0 and 100!")
- out_c1 = round(a_c1 + (b_c1 - a_c1) * percent / 100)
- out_c2 = round(a_c2 + (b_c2 - a_c2) * percent / 100)
- out_c3 = round(a_c3 + (b_c3 - a_c3) * percent / 100)
- return (out_c1, out_c2, out_c3)
-
-
-def interpolate_color(
- start: QColor,
- end: QColor,
- percent: int,
- colorspace: typing.Optional[QColor.Spec] = QColor.Rgb
-) -> QColor:
- """Get an interpolated color value.
-
- Args:
- start: The start color.
- end: The end color.
- percent: Which value to get (0 - 100)
- colorspace: The desired interpolation color system,
- QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum)
- If None, start is used except when percent is 100.
-
- Return:
- The interpolated QColor, with the same spec as the given start color.
- """
- qtutils.ensure_valid(start)
- qtutils.ensure_valid(end)
-
- if colorspace is None:
- if percent == 100:
- return QColor(*end.getRgb())
- else:
- return QColor(*start.getRgb())
-
- out = QColor()
- if colorspace == QColor.Rgb:
- a_c1, a_c2, a_c3, _alpha = start.getRgb()
- b_c1, b_c2, b_c3, _alpha = end.getRgb()
- components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3,
- percent)
- out.setRgb(*components)
- elif colorspace == QColor.Hsv:
- a_c1, a_c2, a_c3, _alpha = start.getHsv()
- b_c1, b_c2, b_c3, _alpha = end.getHsv()
- components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3,
- percent)
- out.setHsv(*components)
- elif colorspace == QColor.Hsl:
- a_c1, a_c2, a_c3, _alpha = start.getHsl()
- b_c1, b_c2, b_c3, _alpha = end.getHsl()
- components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3,
- percent)
- out.setHsl(*components)
- else:
- raise ValueError("Invalid colorspace!")
- out = out.convertTo(start.spec())
- qtutils.ensure_valid(out)
- return out
+def parse_version(version: str) -> VersionNumber:
+ """Parse a version string."""
+ v_q, _suffix = QVersionNumber.fromString(version)
+ return cast(VersionNumber, v_q.normalized())
def format_seconds(total_seconds: int) -> str:
@@ -303,9 +261,7 @@ def format_seconds(total_seconds: int) -> str:
return prefix + ':'.join(chunks)
-def format_size(size: typing.Optional[float],
- base: int = 1024,
- suffix: str = '') -> str:
+def format_size(size: Optional[float], base: int = 1024, suffix: str = '') -> str:
"""Format a byte size so it's human readable.
Inspired by http://stackoverflow.com/q/1094841
@@ -324,13 +280,13 @@ class FakeIOStream(io.TextIOBase):
"""A fake file-like stream which calls a function for write-calls."""
- def __init__(self, write_func: typing.Callable[[str], int]) -> None:
+ def __init__(self, write_func: Callable[[str], int]) -> None:
super().__init__()
self.write = write_func # type: ignore[assignment]
@contextlib.contextmanager
-def fake_io(write_func: typing.Callable[[str], int]) -> typing.Iterator[None]:
+def fake_io(write_func: Callable[[str], int]) -> Iterator[None]:
"""Run code with stdout and stderr replaced by FakeIOStreams.
Args:
@@ -354,7 +310,7 @@ def fake_io(write_func: typing.Callable[[str], int]) -> typing.Iterator[None]:
@contextlib.contextmanager
-def disabled_excepthook() -> typing.Iterator[None]:
+def disabled_excepthook() -> Iterator[None]:
"""Run code with the exception hook temporarily disabled."""
old_excepthook = sys.excepthook
sys.excepthook = sys.__excepthook__
@@ -387,7 +343,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
_predicate: The condition which needs to be True to prevent exceptions
"""
- def __init__(self, retval: typing.Any, predicate: bool = True) -> None:
+ def __init__(self, retval: Any, predicate: bool = True) -> None:
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
@@ -398,7 +354,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
self._retval = retval
self._predicate = predicate
- def __call__(self, func: typing.Callable) -> typing.Callable:
+ def __call__(self, func: Callable) -> Callable:
"""Called when a function should be decorated.
Args:
@@ -413,7 +369,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
retval = self._retval
@functools.wraps(func)
- def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
"""Call the original function."""
try:
return func(*args, **kwargs)
@@ -424,7 +380,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
return wrapper
-def is_enum(obj: typing.Any) -> bool:
+def is_enum(obj: Any) -> bool:
"""Check if a given object is an enum."""
try:
return issubclass(obj, enum.Enum)
@@ -432,9 +388,7 @@ def is_enum(obj: typing.Any) -> bool:
return False
-def get_repr(obj: typing.Any,
- constructor: bool = False,
- **attrs: typing.Any) -> str:
+def get_repr(obj: Any, constructor: bool = False, **attrs: Any) -> str:
"""Get a suitable __repr__ string for an object.
Args:
@@ -457,7 +411,7 @@ def get_repr(obj: typing.Any,
return '<{}>'.format(cls)
-def qualname(obj: typing.Any) -> str:
+def qualname(obj: Any) -> str:
"""Get the fully qualified name of an object.
Based on twisted.python.reflect.fullyQualifiedName.
@@ -485,14 +439,10 @@ def qualname(obj: typing.Any) -> str:
return repr(obj)
-# The string annotation is a WORKAROUND for a Python 3.5.2 bug:
-# https://github.com/python/typing/issues/266
+_ExceptionType = Union[Type[BaseException], Tuple[Type[BaseException]]]
-def raises(exc: ('typing.Union[' # pylint: disable=bad-docstring-quotes
- ' typing.Type[BaseException], '
- ' typing.Tuple[typing.Type[BaseException]]]'),
- func: typing.Callable,
- *args: typing.Any) -> bool:
+
+def raises(exc: _ExceptionType, func: Callable, *args: Any) -> bool:
"""Check if a function raises a given exception.
Args:
@@ -520,7 +470,7 @@ def force_encoding(text: str, encoding: str) -> str:
def sanitize_filename(name: str,
- replacement: typing.Optional[str] = '_',
+ replacement: Optional[str] = '_',
shorten: bool = False) -> str:
"""Replace invalid filename characters.
@@ -641,15 +591,6 @@ def supports_selection() -> bool:
return QApplication.clipboard().supportsSelection()
-def random_port() -> 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
-
-
def open_file(filename: str, cmdline: str = None) -> None:
"""Open the given file.
@@ -708,7 +649,7 @@ def open_file(filename: str, cmdline: str = None) -> None:
proc.start_detached(cmd, args)
-def unused(_arg: typing.Any) -> None:
+def unused(_arg: Any) -> None:
"""Function which does nothing to avoid pylint complaining."""
@@ -730,16 +671,22 @@ def expand_windows_drive(path: str) -> str:
return path
-def yaml_load(f: typing.Union[str, typing.IO[str]]) -> typing.Any:
+def yaml_load(f: Union[str, IO[str]]) -> Any:
"""Wrapper over yaml.load using the C loader if possible."""
start = datetime.datetime.now()
# WORKAROUND for https://github.com/yaml/pyyaml/pull/181
- with log.ignore_py_warnings(
+ with log.py_warning_filter(
category=DeprecationWarning,
message=r"Using or importing the ABCs from 'collections' instead "
r"of from 'collections\.abc' is deprecated.*"):
- data = yaml.load(f, Loader=YamlLoader)
+ try:
+ data = yaml.load(f, Loader=YamlLoader)
+ except ValueError as e:
+ if str(e).startswith('could not convert string to float'):
+ # WORKAROUND for https://github.com/yaml/pyyaml/issues/168
+ raise yaml.YAMLError(e)
+ raise # pragma: no cover
end = datetime.datetime.now()
@@ -760,8 +707,7 @@ def yaml_load(f: typing.Union[str, typing.IO[str]]) -> typing.Any:
return data
-def yaml_dump(data: typing.Any,
- f: typing.IO[str] = None) -> typing.Optional[str]:
+def yaml_dump(data: Any, f: IO[str] = None) -> Optional[str]:
"""Wrapper over yaml.dump using the C dumper if possible.
Also returns a str instead of bytes.
@@ -774,7 +720,7 @@ def yaml_dump(data: typing.Any,
return yaml_data.decode('utf-8')
-def chunk(elems: typing.Sequence, n: int) -> typing.Iterator[typing.Sequence]:
+def chunk(elems: Sequence, n: int) -> Iterator[Sequence]:
"""Yield successive n-sized chunks from elems.
If elems % n != 0, the last chunk will be smaller.
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 90903af20..64efe4c4f 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -30,11 +30,10 @@ import collections
import enum
import datetime
import getpass
-import typing
import functools
+from typing import Mapping, Optional, Sequence, Tuple, cast
import attr
-import pkg_resources
from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo
from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile,
@@ -82,20 +81,36 @@ class DistributionInfo:
"""Information about the running distribution."""
- id = attr.ib() # type: typing.Optional[str]
- parsed = attr.ib() # type: Distribution
- version = attr.ib() # type: typing.Optional[typing.Tuple[str, ...]]
- pretty = attr.ib() # type: str
+ id: Optional[str] = attr.ib()
+ parsed: 'Distribution' = attr.ib()
+ version: Optional[utils.VersionNumber] = attr.ib()
+ pretty: str = attr.ib()
pastebin_url = None
-Distribution = enum.Enum(
- 'Distribution', ['unknown', 'ubuntu', 'debian', 'void', 'arch',
- 'gentoo', 'fedora', 'opensuse', 'linuxmint', 'manjaro',
- 'kde_flatpak'])
-def distribution() -> typing.Optional[DistributionInfo]:
+class Distribution(enum.Enum):
+
+ """A known Linux distribution.
+
+ Usually lines up with ID=... in /etc/os-release.
+ """
+
+ unknown = enum.auto()
+ ubuntu = enum.auto()
+ debian = enum.auto()
+ void = enum.auto()
+ arch = enum.auto()
+ gentoo = enum.auto() # includes funtoo
+ fedora = enum.auto()
+ opensuse = enum.auto()
+ linuxmint = enum.auto()
+ manjaro = enum.auto()
+ kde_flatpak = enum.auto() # org.kde.Platform
+
+
+def distribution() -> Optional[DistributionInfo]:
"""Get some information about the running Linux distribution.
Returns:
@@ -123,9 +138,8 @@ def distribution() -> typing.Optional[DistributionInfo]:
assert pretty is not None
if 'VERSION_ID' in info:
- dist_version = pkg_resources.parse_version(
- info['VERSION_ID']
- ) # type: typing.Optional[typing.Tuple[str, ...]]
+ version_id = info['VERSION_ID']
+ dist_version: Optional[utils.VersionNumber] = utils.parse_version(version_id)
else:
dist_version = None
@@ -154,7 +168,7 @@ def is_sandboxed() -> bool:
return current_distro.parsed == Distribution.kde_flatpak
-def _git_str() -> typing.Optional[str]:
+def _git_str() -> Optional[str]:
"""Try to find out git version.
Return:
@@ -188,7 +202,7 @@ def _call_git(gitpath: str, *args: str) -> str:
stdout=subprocess.PIPE).stdout.decode('UTF-8').strip()
-def _git_str_subprocess(gitpath: str) -> typing.Optional[str]:
+def _git_str_subprocess(gitpath: str) -> Optional[str]:
"""Try to get the git commit ID and timestamp by calling git.
Args:
@@ -210,7 +224,7 @@ def _git_str_subprocess(gitpath: str) -> typing.Optional[str]:
return None
-def _release_info() -> typing.Sequence[typing.Tuple[str, str]]:
+def _release_info() -> Sequence[Tuple[str, str]]:
"""Try to gather distribution release information.
Return:
@@ -234,26 +248,25 @@ def _release_info() -> typing.Sequence[typing.Tuple[str, str]]:
return data
-def _module_versions() -> typing.Sequence[str]:
+def _module_versions() -> Sequence[str]:
"""Get versions of optional modules.
Return:
A list of lines with version info.
"""
lines = []
- modules = collections.OrderedDict([
+ modules: Mapping[str, Sequence[str]] = collections.OrderedDict([
('sip', ['SIP_VERSION_STR']),
('colorama', ['VERSION', '__version__']),
('pypeg2', ['__version__']),
('jinja2', ['__version__']),
('pygments', ['__version__']),
('yaml', ['__version__']),
- ('cssutils', ['__version__']),
('attr', ['__version__']),
('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']),
('PyQt5.QtWebKitWidgets', []),
- ]) # type: typing.Mapping[str, typing.Sequence[str]]
+ ])
for modname, attributes in modules.items():
try:
module = importlib.import_module(modname)
@@ -273,7 +286,7 @@ def _module_versions() -> typing.Sequence[str]:
return lines
-def _path_info() -> typing.Mapping[str, str]:
+def _path_info() -> Mapping[str, str]:
"""Get info about important path names.
Return:
@@ -292,7 +305,7 @@ def _path_info() -> typing.Mapping[str, str]:
return info
-def _os_info() -> typing.Sequence[str]:
+def _os_info() -> Sequence[str]:
"""Get operating system info.
Return:
@@ -355,41 +368,6 @@ def _chromium_version() -> str:
Quick reference:
- Qt 5.7: Chromium 49
- 49.0.2623.111 (2016-03-31)
- 5.7.0: Security fixes from Chromium 50 and 51
- 5.7.1: Security fixes up to 54.0.2840.87 (2016-11-01)
-
- Qt 5.8: Chromium 53
- 53.0.2785.148 (2016-08-31)
- 5.8.0: Security fixes up to 55.0.2883.75 (2016-12-01)
-
- Qt 5.9: Chromium 56
- (LTS) 56.0.2924.122 (2017-01-25)
- 5.9.0: Security fixes up to 56.0.2924.122 (?)
- 5.9.1: Security fixes up to 59.0.3071.104 (2017-06-15)
- 5.9.2: Security fixes up to 61.0.3163.79 (2017-09-05)
- 5.9.3: Security fixes up to 62.0.3202.89 (2017-11-06)
- 5.9.4: Security fixes up to 63.0.3239.132 (~2017-12-14)
- 5.9.5: Security fixes up to 65.0.3325.146 (~2018-03-13)
- 5.9.6: Security fixes up to 66.0.3359.170 (2018-05-10)
- 5.9.7: Security fixes up to 69.0.3497.113 (~2018-09-11)
- 5.9.8: Security fixes up to 72.0.3626.121 (2019-03-01)
- 5.9.9: Security fixes up to 78.0.3904.108 (2019-11-18)
-
- Qt 5.10: Chromium 61
- 61.0.3163.140 (2017-09-05)
- 5.10.0: Security fixes up to 62.0.3202.94 (2017-11-13)
- 5.10.1: Security fixes up to 64.0.3282.140 (2018-02-01)
-
- Qt 5.11: Chromium 65
- 65.0.3325.151 (2018-03-06)
- 5.11.0: Security fixes up to 66.0.3359.139 (2018-04-26)
- 5.11.1: Updated to 65.0.3325.15.230
- Security fixes up to 67.0.3396.87 (2018-06-12)
- 5.11.2: Security fixes up to 68.0.3440.75 (~2018-07-31)
- 5.11.3: Security fixes up to 70.0.3538.102 (2018-11-09)
-
Qt 5.12: Chromium 69
(LTS) 69.0.3497.128 (~2018-09-11)
5.12.0: Security fixes up to 70.0.3538.102 (~2018-10-24)
@@ -402,6 +380,7 @@ def _chromium_version() -> str:
5.12.7: Security fixes up to 79.0.3945.130 (2020-01-16)
5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18)
5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03)
+ 5.12.10: Security fixes up to 86.0.4240.75 (2020-10-06)
Qt 5.13: Chromium 73
73.0.3683.105 (~2019-02-28)
@@ -418,6 +397,10 @@ def _chromium_version() -> str:
Qt 5.15: Chromium 80
80.0.3987.163 (2020-04-02)
5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05)
+ 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25)
+
+ 5.15.2: Updated to 83.0.4103.122 (~2020-06-24)
+ Security fixes up to 86.0.4240.183 (2020-11-02)
Also see:
@@ -429,6 +412,8 @@ def _chromium_version() -> str:
return 'unavailable' # type: ignore[unreachable]
if webenginesettings.parsed_user_agent is None:
+ if 'avoid-chromium-init' in objects.debug_flags:
+ return 'avoided'
webenginesettings.init_user_agent()
assert webenginesettings.parsed_user_agent is not None
@@ -549,21 +534,21 @@ class OpenGLInfo:
"""Information about the OpenGL setup in use."""
# If we're using OpenGL ES. If so, no further information is available.
- gles = attr.ib(False) # type: bool
+ gles: bool = attr.ib(False)
# The name of the vendor. Examples:
# - nouveau
# - "Intel Open Source Technology Center", "Intel", "Intel Inc."
- vendor = attr.ib(None) # type: typing.Optional[str]
+ vendor: Optional[str] = attr.ib(None)
# The OpenGL version as a string. See tests for examples.
- version_str = attr.ib(None) # type: typing.Optional[str]
+ version_str: Optional[str] = attr.ib(None)
# The parsed version as a (major, minor) tuple of ints
- version = attr.ib(None) # type: typing.Optional[typing.Tuple[int, ...]]
+ version: Optional[Tuple[int, ...]] = attr.ib(None)
# The vendor specific information following the version number
- vendor_specific = attr.ib(None) # type: typing.Optional[str]
+ vendor_specific: Optional[str] = attr.ib(None)
def __str__(self) -> str:
if self.gles:
@@ -601,7 +586,7 @@ class OpenGLInfo:
@functools.lru_cache(maxsize=1)
-def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover
+def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
"""Get the OpenGL vendor used.
This returns a string such as 'nouveau' or
@@ -619,8 +604,7 @@ def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover
vendor, version = override.split(', ', maxsplit=1)
return OpenGLInfo.parse(vendor=vendor, version=version)
- old_context = typing.cast(typing.Optional[QOpenGLContext],
- QOpenGLContext.currentContext())
+ old_context = cast(Optional[QOpenGLContext], QOpenGLContext.currentContext())
old_surface = None if old_context is None else old_context.surface()
surface = QOffscreenSurface()
diff --git a/requirements.txt b/requirements.txt
index c3835488c..fee8906ae 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==20.1.0
-colorama==0.4.3
-cssutils==1.0.2
+attrs==20.3.0
+colorama==0.4.4
Jinja2==2.11.2
MarkupSafe==1.1.1
-Pygments==2.6.1
+Pygments==2.7.2
pyPEG2==2.15.2
PyYAML==5.3.1
diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py
index 5cb49c767..109e61625 100755
--- a/scripts/asciidoc2html.py
+++ b/scripts/asciidoc2html.py
@@ -46,14 +46,16 @@ class AsciiDoc:
FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts']
def __init__(self,
- asciidoc: Optional[List[str]],
+ asciidoc: Optional[str],
+ asciidoc_python: Optional[str],
website: Optional[str]) -> None:
- self._cmd = None # type: Optional[List[str]]
+ self._cmd: Optional[List[str]] = None
self._asciidoc = asciidoc
+ self._asciidoc_python = asciidoc_python
self._website = website
- self._homedir = None # type: Optional[pathlib.Path]
- self._themedir = None # type: Optional[pathlib.Path]
- self._tempdir = None # type: Optional[pathlib.Path]
+ self._homedir: Optional[pathlib.Path] = None
+ self._themedir: Optional[pathlib.Path] = None
+ self._tempdir: Optional[pathlib.Path] = None
self._failed = False
def prepare(self) -> None:
@@ -218,7 +220,9 @@ class AsciiDoc:
def _get_asciidoc_cmd(self) -> List[str]:
"""Try to find out what commandline to use to invoke asciidoc."""
if self._asciidoc is not None:
- return self._asciidoc
+ python = (sys.executable if self._asciidoc_python is None
+ else self._asciidoc_python)
+ return [python, self._asciidoc]
for executable in ['asciidoc', 'asciidoc.py']:
try:
@@ -270,10 +274,12 @@ def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument('--website', help="Build website into a given "
"directory.")
- parser.add_argument('--asciidoc', help="Full path to python and "
- "asciidoc.py. If not given, it's searched in PATH.",
- nargs=2, required=False,
- metavar=('PYTHON', 'ASCIIDOC'))
+ parser.add_argument('--asciidoc', help="Full path to asciidoc.py. "
+ "If not given, it's searched in PATH.",
+ nargs='?')
+ parser.add_argument('--asciidoc-python', help="Python to use for asciidoc."
+ "If not given, the current Python interpreter is used.",
+ nargs='?')
return parser.parse_args()
@@ -301,7 +307,8 @@ def main(colors: bool = False) -> None:
utils.change_cwd()
utils.use_color = colors
args = parse_args()
- run(asciidoc=args.asciidoc, website=args.website)
+ run(asciidoc=args.asciidoc, asciidoc_python=args.asciidoc_python,
+ website=args.website)
if __name__ == '__main__':
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index ee0ac2c53..6044a1e18 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -78,10 +78,11 @@ def call_tox(toxenv, *args, python=sys.executable):
def run_asciidoc2html(args):
"""Common buildsteps used for all OS'."""
utils.print_title("Running asciidoc2html.py")
+ a2h_args = []
if args.asciidoc is not None:
- a2h_args = ['--asciidoc'] + args.asciidoc
- else:
- a2h_args = []
+ a2h_args += ['--asciidoc', args.asciidoc]
+ if args.asciidoc_python is not None:
+ a2h_args += ['--asciidoc-python', args.asciidoc_python]
call_script('asciidoc2html.py', *a2h_args)
@@ -201,7 +202,7 @@ def build_mac():
utils.print_title("Updating 3rdparty content")
update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
utils.print_title("Building .app via pyinstaller")
- call_tox('pyinstaller', '-r')
+ call_tox('pyinstaller-64', '-r')
utils.print_title("Patching .app")
patch_mac_app()
utils.print_title("Building .dmg")
@@ -251,7 +252,7 @@ def _get_windows_python_path(x64):
return fallback
-def build_windows():
+def build_windows(*, skip_packaging):
"""Build windows executables/setups."""
utils.print_title("Updating 3rdparty content")
update_3rdparty.run(nsis=True, ace=False, pdfjs=True, fancy_dmg=False)
@@ -288,6 +289,14 @@ def build_windows():
utils.print_title("Running 64bit smoke test")
smoke_test(os.path.join(out_64, 'qutebrowser.exe'))
+ if not skip_packaging:
+ artifacts += _package_windows(out_32, out_64)
+
+ return artifacts
+
+
+def _package_windows(out_32, out_64):
+ """Build installers/zips for Windows."""
utils.print_title("Building installers")
subprocess.run(['makensis.exe',
'/DVERSION={}'.format(qutebrowser.__version__),
@@ -300,7 +309,7 @@ def build_windows():
name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__)
name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
- artifacts += [
+ artifacts = [
(os.path.join('dist', name_32),
'application/vnd.microsoft.portable-executable',
'Windows 32bit installer'),
@@ -457,12 +466,16 @@ def main():
parser = argparse.ArgumentParser()
parser.add_argument('--no-asciidoc', action='store_true',
help="Don't generate docs")
- parser.add_argument('--asciidoc', help="Full path to python and "
- "asciidoc.py. If not given, it's searched in PATH.",
- nargs=2, required=False,
- metavar=('PYTHON', 'ASCIIDOC'))
+ parser.add_argument('--asciidoc', help="Full path to asciidoc.py. "
+ "If not given, it's searched in PATH.",
+ nargs='?')
+ parser.add_argument('--asciidoc-python', help="Python to use for asciidoc."
+ "If not given, the current Python interpreter is used.",
+ nargs='?')
parser.add_argument('--upload', action='store_true', required=False,
- help="Toggle to upload the release to GitHub")
+ help="Toggle to upload the release to GitHub.")
+ parser.add_argument('--skip-packaging', action='store_true', required=False,
+ help="Skip Windows installer/zip generation.")
args = parser.parse_args()
utils.change_cwd()
@@ -484,7 +497,7 @@ def main():
run_asciidoc2html(args)
if os.name == 'nt':
- artifacts = build_windows()
+ artifacts = build_windows(skip_packaging=args.skip_packaging)
elif sys.platform == 'darwin':
artifacts = build_mac()
else:
diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py
index 313aa13e3..728a36873 100644
--- a/scripts/dev/check_coverage.py
+++ b/scripts/dev/check_coverage.py
@@ -53,7 +53,12 @@ class Message:
print(self.text)
-MsgType = enum.Enum('MsgType', 'insufficient_coverage, perfect_file')
+class MsgType(enum.Enum):
+
+ """The type of a message to be output."""
+
+ insufficient_coverage = enum.auto()
+ perfect_file = enum.auto()
# A list of (test_file, tested_file) tuples. test_file can be None.
@@ -73,6 +78,8 @@ PERFECT_FILES = [
'qutebrowser/api/message.py'),
(None,
'qutebrowser/api/qtutils.py'),
+ (None,
+ 'qutebrowser/qt.py'),
('tests/unit/browser/webkit/test_cache.py',
'qutebrowser/browser/webkit/cache.py'),
@@ -213,6 +220,8 @@ PERFECT_FILES = [
'qutebrowser/browser/webengine/spell.py'),
('tests/unit/browser/webengine/test_webengine_cookies.py',
'qutebrowser/browser/webengine/cookies.py'),
+ ('tests/unit/browser/webengine/test_darkmode.py',
+ 'qutebrowser/browser/webengine/darkmode.py'),
]
@@ -324,8 +333,9 @@ def main_check():
subprocess.run([sys.executable, '-m', 'coverage', 'report',
'--show-missing', '--include', filters], check=True)
print()
- print("To debug this, run 'tox -e py36-pyqt59-cov' "
- "(or py35-pyqt59-cov) locally and check htmlcov/index.html")
+ print("To debug this, run 'tox -e py36-pyqt515-cov' "
+ "(replace Python/Qt versions based on your system) locally and check "
+ "htmlcov/index.html")
print("or check https://codecov.io/github/qutebrowser/qutebrowser")
print()
diff --git a/scripts/dev/ci/docker/Dockerfile.j2 b/scripts/dev/ci/docker/Dockerfile.j2
new file mode 100644
index 000000000..1835f0a2f
--- /dev/null
+++ b/scripts/dev/ci/docker/Dockerfile.j2
@@ -0,0 +1,27 @@
+FROM thecompiler/archlinux
+MAINTAINER Florian Bruhin <me@the-compiler.org>
+
+{% if unstable %}
+RUN sed -i '/^# after the header/a[kde-unstable]\nInclude = /etc/pacman.d/mirrorlist\n\n[testing]\nInclude = /etc/pacman.d/mirrorlist' /etc/pacman.conf
+{% endif %}
+RUN pacman -Suyy --noconfirm \
+ git \
+ python-tox \
+ python-distlib \
+ qt5-base \
+ qt5-declarative \
+ {% if webengine %}qt5-webengine python-pyqtwebengine{% else %}qt5-webkit{% endif %} \
+ python-pyqt5 \
+ xorg-xinit \
+ xorg-server-xvfb \
+ ttf-bitstream-vera \
+ gcc \
+ libyaml \
+ xorg-xdpyinfo
+
+USER user
+WORKDIR /home/user
+
+CMD git clone /outside qutebrowser.git && \
+ cd qutebrowser.git && \
+ tox -e py
diff --git a/scripts/dev/ci/docker/README.md b/scripts/dev/ci/docker/README.md
new file mode 100644
index 000000000..eb2b8db91
--- /dev/null
+++ b/scripts/dev/ci/docker/README.md
@@ -0,0 +1,9 @@
+This directory contains a Dockerfile template for containers used to test
+qutebrowser on CI.
+
+The `generate.py` script uses that template to generate various image
+configuration.
+
+The images are rebuilt via Github Actions in this directory, and qutebrowser
+then downloads them during the CI run. Note that means that it'll take a while
+until builds will use the newer image if you make a change to this directory.
diff --git a/scripts/dev/ci/docker/generate.py b/scripts/dev/ci/docker/generate.py
new file mode 100644
index 000000000..7d09fdb20
--- /dev/null
+++ b/scripts/dev/ci/docker/generate.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2019-2020 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 <http://www.gnu.org/licenses/>.
+
+"""Generate Dockerfiles for qutebrowser's CI."""
+
+import sys
+
+import jinja2
+
+
+def main():
+ with open('Dockerfile.j2') as f:
+ template = jinja2.Template(f.read())
+
+ image = sys.argv[1]
+ config = {
+ 'archlinux-webkit': {'webengine': False, 'unstable': False},
+ 'archlinux-webengine': {'webengine': True, 'unstable': False},
+ 'archlinux-webengine-unstable': {'webengine': True, 'unstable': True},
+ }[image]
+
+ with open('Dockerfile', 'w') as f:
+ f.write(template.render(**config))
+ f.write('\n')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/dev/ci/problemmatchers.py b/scripts/dev/ci/problemmatchers.py
index 320d0deeb..083945051 100644
--- a/scripts/dev/ci/problemmatchers.py
+++ b/scripts/dev/ci/problemmatchers.py
@@ -182,6 +182,20 @@ MATCHERS = {
],
},
],
+
+ "misc": [
+ {
+ "severity": "error",
+ "pattern": [
+ {
+ "regexp": r'^([^:]+):(\d+): \033[34m(Found .*)\033[0m',
+ "file": 1,
+ "line": 2,
+ "message": 3,
+ }
+ ]
+ }
+ ]
}
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index 6bb3eb1ca..ad446412c 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -28,36 +28,96 @@ import argparse
import subprocess
import tokenize
import traceback
-import collections
import pathlib
+from typing import List, Iterator, Optional
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
- os.pardir))
+REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(REPO_ROOT))
from scripts import utils
+from scripts.dev import recompile_requirements
+BINARY_EXTS = {'.png', '.icns', '.ico', '.bmp', '.gz', '.bin', '.pdf',
+ '.sqlite', '.woff2', '.whl'}
-def _get_files(only_py=False):
- """Iterate over all python files and yield filenames."""
- for (dirpath, _dirnames, filenames) in os.walk('.'):
- parts = dirpath.split(os.sep)
- if len(parts) >= 2:
- rootdir = parts[1]
- if rootdir.startswith('.') or rootdir == 'htmlcov':
- # ignore hidden dirs and htmlcov
- continue
- if only_py:
- endings = {'.py'}
- else:
- endings = {'.py', '.asciidoc', '.js', '.feature'}
- files = (e for e in filenames if os.path.splitext(e)[1] in endings)
- for name in files:
- yield os.path.join(dirpath, name)
+def _get_files(
+ *,
+ verbose: bool,
+ ignored: List[pathlib.Path] = None
+) -> Iterator[pathlib.Path]:
+ """Iterate over all files and yield filenames."""
+ filenames = subprocess.run(
+ ['git', 'ls-files', '--cached', '--others', '--exclude-standard', '-z'],
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ check=True,
+ )
+ all_ignored = ignored or []
+ all_ignored.append(
+ pathlib.Path('tests', 'unit', 'scripts', 'importer_sample', 'chrome'))
+
+ for filename in filenames.stdout.split('\0'):
+ path = pathlib.Path(filename)
+ is_ignored = any(path == p or p in path.parents for p in all_ignored)
+ if not filename or path.suffix in BINARY_EXTS or is_ignored:
+ continue
+
+ try:
+ with tokenize.open(str(path)):
+ pass
+ except SyntaxError as e:
+ # Could not find encoding
+ utils.print_col("{} - maybe {} should be added to BINARY_EXTS?".format(
+ str(e).capitalize(), path.suffix), 'yellow')
+ continue
+
+ if verbose:
+ print(path)
+
+ yield path
+
+
+def check_changelog_urls(_args: argparse.Namespace = None) -> bool:
+ """Ensure we have changelog URLs for all requirements."""
+ ok = True
+ all_requirements = set()
+
+ for name in recompile_requirements.get_all_names():
+ outfile = recompile_requirements.get_outfile(name)
+ missing = set()
+ with open(outfile, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith('#') or not line:
+ continue
+ req, _version = recompile_requirements.parse_versioned_line(line)
+ if req.startswith('./'):
+ continue
+ all_requirements.add(req)
+ if req not in recompile_requirements.CHANGELOG_URLS:
+ missing.add(req)
+
+ if missing:
+ ok = False
+ req_str = ', '.join(sorted(missing))
+ utils.print_col(
+ f"Missing changelog URLs in {name} requirements: {req_str}", 'red')
+
+ extra = set(recompile_requirements.CHANGELOG_URLS) - all_requirements
+ if extra:
+ ok = False
+ req_str = ', '.join(sorted(extra))
+ utils.print_col(f"Extra changelog URLs: {req_str}", 'red')
+
+ if not ok:
+ print("Hint: Changelog URLs are in scripts/dev/recompile_requirements.py")
+
+ return ok
-def check_git():
- """Check for uncommitted git files.."""
+def check_git(_args: argparse.Namespace = None) -> bool:
+ """Check for uncommitted git files."""
if not os.path.isdir(".git"):
print("No .git dir, ignoring")
print()
@@ -79,7 +139,18 @@ def check_git():
return status
-def check_spelling():
+def _check_spelling_file(path, fobj, patterns):
+ ok = True
+ for num, line in enumerate(fobj, start=1):
+ for pattern, explanation in patterns:
+ if pattern.search(line):
+ ok = False
+ print(f'{path}:{num}: ', end='')
+ utils.print_col(f'Found "{pattern.pattern}" - {explanation}', 'blue')
+ return ok
+
+
+def check_spelling(args: argparse.Namespace) -> Optional[bool]:
"""Check commonly misspelled words."""
# Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
@@ -90,38 +161,60 @@ def check_spelling():
'exitted', 'mininum', 'resett?ed', 'recieved', 'regularily',
'underlaying', 'inexistant', 'elipsis', 'commiting', 'existant',
'resetted', 'similarily', 'informations', 'an url', 'treshold',
- 'artefact'}
+ 'artefact', 'an unix', 'an utf', 'an unicode'}
# Words which look better when splitted, but might need some fine tuning.
words |= {'webelements', 'mouseevent', 'keysequence', 'normalmode',
'eventloops', 'sizehint', 'statemachine', 'metaobject',
- 'logrecord', 'filetype'}
+ 'logrecord'}
+
+ patterns = [
+ (
+ re.compile(r'[{}{}]{}'.format(w[0], w[0].upper(), w[1:])),
+ "Common misspelling or non-US spelling"
+ ) for w in words
+ ]
+ patterns += [
+ (
+ re.compile(r'(?i)# noqa(?!: )'),
+ "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.",
+ ),
+ (
+ re.compile(r': typing\.'),
+ "Don't use typing.SomeType, do 'from typing import SomeType' instead.",
+ ),
+ (
+ re.compile(r"""monkeypatch\.setattr\(['"]"""),
+ "Don't use monkeypatch.setattr('obj.attr', value), use "
+ "setattr(obj, 'attr', value) instead.",
+ ),
+ ]
# Files which should be ignored, e.g. because they come from another
# package
+ hint_data = pathlib.Path('tests', 'end2end', 'data', 'hints')
ignored = [
- os.path.join('.', 'scripts', 'dev', 'misc_checks.py'),
- os.path.join('.', 'qutebrowser', '3rdparty', 'pdfjs'),
- os.path.join('.', 'tests', 'end2end', 'data', 'hints', 'ace',
- 'ace.js'),
+ pathlib.Path('scripts', 'dev', 'misc_checks.py'),
+ pathlib.Path('qutebrowser', '3rdparty', 'pdfjs'),
+ hint_data / 'ace' / 'ace.js',
+ hint_data / 'bootstrap' / 'bootstrap.css',
]
- seen = collections.defaultdict(list)
try:
ok = True
- for fn in _get_files():
- with tokenize.open(fn) as f:
- if any(fn.startswith(i) for i in ignored):
- continue
- for line in f:
- for w in words:
- pattern = '[{}{}]{}'.format(w[0], w[0].upper(), w[1:])
- if (re.search(pattern, line) and
- fn not in seen[w] and
- '# pragma: no spellcheck' not in line):
- print('Found "{}" in {}!'.format(w, fn))
- seen[w].append(fn)
- ok = False
+ for path in _get_files(verbose=args.verbose, ignored=ignored):
+ with tokenize.open(str(path)) as f:
+ if not _check_spelling_file(path, f, patterns):
+ ok = False
print()
return ok
except Exception:
@@ -129,15 +222,18 @@ def check_spelling():
return None
-def check_vcs_conflict():
+def check_vcs_conflict(args: argparse.Namespace) -> Optional[bool]:
"""Check VCS conflict markers."""
try:
ok = True
- for fn in _get_files(only_py=True):
- with tokenize.open(fn) as f:
+ for path in _get_files(verbose=args.verbose):
+ if path.suffix in {'.rst', '.asciidoc'}:
+ # False positives
+ continue
+ with tokenize.open(str(path)) as f:
for line in f:
if any(line.startswith(c * 7) for c in '<>=|'):
- print("Found conflict marker in {}".format(fn))
+ print("Found conflict marker in {}".format(path))
ok = False
print()
return ok
@@ -146,7 +242,7 @@ def check_vcs_conflict():
return None
-def check_userscripts_descriptions():
+def check_userscripts_descriptions(_args: argparse.Namespace = None) -> bool:
"""Make sure all userscripts are described properly."""
folder = pathlib.Path('misc/userscripts')
readme = folder / 'README.md'
@@ -178,20 +274,54 @@ def check_userscripts_descriptions():
return ok
-def main():
+def check_userscript_shebangs(_args: argparse.Namespace) -> bool:
+ """Check that we're using /usr/bin/env in shebangs."""
+ ok = True
+ folder = pathlib.Path('misc/userscripts')
+
+ for sub in folder.iterdir():
+ if sub.is_dir() or sub.name == 'README.md':
+ continue
+
+ with sub.open('r', encoding='utf-8') as f:
+ shebang = f.readline()
+ assert shebang.startswith('#!'), shebang
+ binary = shebang.split()[0][2:]
+
+ if binary not in ['/bin/sh', '/usr/bin/env']:
+ bin_name = pathlib.Path(binary).name
+ print(f"In {sub}, use #!/usr/bin/env {bin_name} instead of #!{binary}")
+ ok = False
+
+ return ok
+
+
+def main() -> int:
+ checkers = {
+ 'git': check_git,
+ 'vcs': check_vcs_conflict,
+ 'spelling': check_spelling,
+ 'userscript-descriptions': check_userscripts_descriptions,
+ 'userscript-shebangs': check_userscript_shebangs,
+ 'changelog-urls': check_changelog_urls,
+ }
+
parser = argparse.ArgumentParser()
+ parser.add_argument('--verbose', action='store_true', help='Show checked filenames')
parser.add_argument('checker',
- choices=('git', 'vcs', 'spelling', 'userscripts'),
+ choices=list(checkers) + ['all'],
help="Which checker to run.")
args = parser.parse_args()
- if args.checker == 'git':
- ok = check_git()
- elif args.checker == 'vcs':
- ok = check_vcs_conflict()
- elif args.checker == 'spelling':
- ok = check_spelling()
- elif args.checker == 'userscripts':
- ok = check_userscripts_descriptions()
+
+ if args.checker == 'all':
+ retvals = []
+ for name, checker in checkers.items():
+ utils.print_title(name)
+ retvals.append(checker(args))
+ return 0 if all(retvals) else 1
+
+ checker = checkers[args.checker]
+ ok = checker(args)
return 0 if ok else 1
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index 4c870cebd..87740c5bb 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -39,50 +39,88 @@ REQ_DIR = os.path.join(REPO_DIR, 'misc', 'requirements')
CHANGELOG_URLS = {
'pyparsing': 'https://github.com/pyparsing/pyparsing/blob/master/CHANGES',
- 'cherrypy': 'https://github.com/cherrypy/cherrypy/blob/master/CHANGES.rst',
'pylint': 'http://pylint.pycqa.org/en/latest/whatsnew/changelog.html',
- 'setuptools': 'https://github.com/pypa/setuptools/blob/master/CHANGES.rst',
+ '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',
+ 'apipkg': 'https://github.com/pytest-dev/apipkg/blob/master/CHANGELOG',
'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://github.com/pallets/werkzeug/blob/master/CHANGES.rst',
+ 'Werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst',
+ 'click': 'https://click.palletsprojects.com/en/7.x/changelog/',
+ 'itsdangerous': 'https://itsdangerous.palletsprojects.com/en/1.1.x/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/1.1.x/changelog/',
+ '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/',
'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',
+ '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',
- 'pip': 'https://pip.pypa.io/en/stable/news/',
- 'packaging': 'https://pypi.org/project/packaging/',
- 'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/',
+ 'packaging': 'https://packaging.pypa.io/en/latest/changelog.html',
+ 'build': 'https://github.com/pypa/build/commits/master',
'attrs': 'http://www.attrs.org/en/stable/changelog.html',
- 'jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst',
+ 'Jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst',
+ 'MarkupSafe': 'https://markupsafe.palletsprojects.com/en/1.1.x/changes/',
'flake8': 'https://gitlab.com/pycqa/flake8/tree/master/docs/source/release-notes',
- 'cffi': 'https://cffi.readthedocs.io/en/latest/whatsnew.html',
+ '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://cffi.readthedocs.io/en/latest/whatsnew.html',
'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://pypi.org/project/uritemplate/',
- 'flake8-builtins': 'https://github.com/gforcada/flake8-builtins/blob/master/CHANGES.rst',
- 'flake8-bugbear': 'https://github.com/PyCQA/flake8-bugbear',
- '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',
+ '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': 'http://www.pydocstyle.org/en/latest/release_notes.html',
- 'sphinx': 'https://www.sphinx-doc.org/en/master/changes.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',
- 'wcwidth': 'https://github.com/jquast/wcwidth#history',
'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',
@@ -90,22 +128,21 @@ CHANGELOG_URLS = {
'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',
- 'flake8-comprehensions': 'https://github.com/adamchainz/flake8-comprehensions/blob/master/HISTORY.rst',
'altgraph': 'https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst',
'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst',
- 'wheel': 'https://github.com/pypa/wheel/blob/master/docs/news.rst',
- 'mako': 'https://docs.makotemplates.org/en/latest/changelog.html',
- 'lxml': 'https://lxml.de/4.5/changes-4.5.0.html',
+ 'lxml': 'https://lxml.de/index.html#old-versions',
'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master',
- 'tox-pip-version': 'https://github.com/pglass/tox-pip-version/commits/master',
'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst',
- 'pep517': 'https://github.com/pypa/pep517/commits/master',
- 'cryptography': 'https://cryptography.io/en/latest/changelog/',
+ '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',
- 'pyqt': 'https://www.riverbankcomputing.com/news',
+ 'PyQt5': 'https://www.riverbankcomputing.com/news',
+ 'PyQtWebEngine': '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',
@@ -114,12 +151,31 @@ CHANGELOG_URLS = {
'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',
+ 'typing-extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
+ 'diff-cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG',
+ 'pytest-clarity': 'https://github.com/darrenburns/pytest-clarity/commits/master',
+ 'pytest-icdiff': 'https://github.com/hjwp/pytest-icdiff/blob/master/HISTORY.rst',
+ 'icdiff': 'https://github.com/jeffkaufman/icdiff/blob/master/ChangeLog',
+ 'termcolor': 'https://pypi.org/project/termcolor/',
+ '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://github3py.readthedocs.io/en/master/release-notes/index.html',
+ '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',
+ 'pyPEG2': None,
}
-# PyQt versions which need SIP v4
-OLD_PYQT = {'pyqt-5.7', 'pyqt-5.9', 'pyqt-5.10', 'pyqt-5.11'}
-
def convert_line(line, comments):
"""Convert the given requirement line to place into the output."""
@@ -205,14 +261,6 @@ def get_all_names():
yield basename[len('requirements-'):-len('.txt-raw')]
-def filter_names(names, old_pyqt=False):
- """Filter requirement names."""
- if old_pyqt:
- return sorted(names)
- else:
- return sorted(set(names) - OLD_PYQT)
-
-
def run_pip(venv_dir, *args, **kwargs):
"""Run pip inside the virtualenv."""
arg_str = ' '.join(str(arg) for arg in args)
@@ -243,9 +291,6 @@ def init_venv(host_python, venv_dir, requirements, pre=False):
def parse_args():
"""Parse commandline arguments via argparse."""
parser = argparse.ArgumentParser()
- parser.add_argument('--old-pyqt',
- action='store_true',
- help='Also include old PyQt requirements.')
parser.add_argument('names', nargs='*')
return parser.parse_args()
@@ -269,8 +314,8 @@ class Change:
self.name = name
self.old = None
self.new = None
- if name.lower() in CHANGELOG_URLS:
- self.url = CHANGELOG_URLS[name.lower()]
+ if CHANGELOG_URLS.get(name):
+ self.url = CHANGELOG_URLS[name]
self.link = '[{}]({})'.format(self.name, self.url)
else:
self.url = '(no changelog)'
@@ -296,8 +341,8 @@ class Change:
return '| {} | {} | {} |'.format(self.link, self.old, self.new)
-def print_changed_files():
- """Output all changed files from this run."""
+def _get_changed_files():
+ """Get a list of changed files via git."""
changed_files = set()
filenames = git_diff('--name-only')
for filename in filenames:
@@ -305,8 +350,32 @@ def print_changed_files():
filename = filename.replace('misc/requirements/requirements-', '')
filename = filename.replace('.txt', '')
changed_files.add(filename)
- files_text = '\n'.join('- ' + line for line in sorted(changed_files))
+ return sorted(changed_files)
+
+
+def parse_versioned_line(line):
+ """Parse a requirements.txt line into name/version."""
+ if '==' in line:
+ line = line.rsplit('#', maxsplit=1)[0] # Strip comments
+ name, version = line.split('==')
+ if ';' in version: # pip environment markers
+ version = version.split(';')[0].strip()
+ elif line.startswith('-e'):
+ rest, name = line.split('#egg=')
+ version = rest.split('@')[1][:7]
+ else:
+ name = line
+ version = '?'
+
+ if name.startswith('#'): # duplicate requirements
+ name = name[1:].strip()
+
+ return name, version
+
+
+def _get_changes():
+ """Get a list of changed versions from git."""
changes_dict = {}
diff = git_diff()
for line in diff:
@@ -315,11 +384,7 @@ def print_changed_files():
if line.startswith('+++ ') or line.startswith('--- '):
continue
- if '==' in line:
- name, version = line[1:].split('==')
- else:
- name = line[1:]
- version = '?'
+ name, version = parse_versioned_line(line[1:])
if name not in changes_dict:
changes_dict[name] = Change(name)
@@ -329,7 +394,15 @@ def print_changed_files():
elif line.startswith('+'):
changes_dict[name].new = version
- changes = [change for _name, change in sorted(changes_dict.items())]
+ return [change for _name, change in sorted(changes_dict.items())]
+
+
+def print_changed_files():
+ """Output all changed files from this run."""
+ changed_files = _get_changed_files()
+ files_text = '\n'.join('- ' + line for line in changed_files)
+
+ changes = _get_changes()
diff_text = '\n'.join(str(change) for change in changes)
utils.print_title('Changed')
@@ -355,15 +428,21 @@ def print_changed_files():
def get_host_python(name):
"""Get the Python to use for a given requirement name.
- Old PyQt versions need sip v4 which doesn't work on Python 3.8
- ylint installs typed_ast on < 3.8 only
+ pylint installs typed_ast on < 3.8 only
"""
- if name in OLD_PYQT or name == 'pylint':
+ if name == 'pylint':
return 'python3.7'
else:
return sys.executable
+def get_outfile(name):
+ """Get the path to the output requirements.txt file."""
+ if name == 'qutebrowser':
+ return os.path.join(REPO_DIR, 'requirements.txt')
+ return os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name))
+
+
def build_requirements(name):
"""Build a requirements file."""
utils.print_subtitle("Building")
@@ -384,10 +463,7 @@ def build_requirements(name):
if utils.ON_CI:
print(reqs.strip())
- if name == 'qutebrowser':
- outfile = os.path.join(REPO_DIR, 'requirements.txt')
- else:
- outfile = os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name))
+ outfile = get_outfile(name)
with open(outfile, 'w', encoding='utf-8') as f:
f.write("# This file is automatically generated by "
@@ -446,7 +522,7 @@ def main():
if args.names:
names = args.names
else:
- names = filter_names(get_all_names(), old_pyqt=args.old_pyqt)
+ names = sorted(get_all_names())
utils.print_col('Rebuilding requirements: ' + ', '.join(names), 'green')
for name in names:
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index f069d50de..5e42febd4 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -32,7 +32,7 @@ import vulture
import qutebrowser.app # pylint: disable=unused-import
from qutebrowser.extensions import loader
from qutebrowser.misc import objects
-from qutebrowser.utils import utils
+from qutebrowser.utils import utils, version
from qutebrowser.browser.webkit import rfc6266
# To run the decorators from there
# pylint: disable=unused-import
@@ -42,7 +42,7 @@ from qutebrowser.browser import qutescheme
from qutebrowser.config import configtypes
-def whitelist_generator(): # noqa
+def whitelist_generator(): # noqa: C901
"""Generator which yields lines to add to a vulture whitelist."""
loader.load_components(skip_hooks=True)
@@ -124,6 +124,9 @@ def whitelist_generator(): # noqa
'_get_default_metavar_for_positional', '_metavar_formatter']:
yield 'scripts.dev.src2asciidoc.UsageFormatter.' + attr
+ for dist in version.Distribution:
+ yield 'qutebrowser.utils.version.Distribution.{}'.format(dist.name)
+
# attrs
yield 'qutebrowser.browser.webkit.network.networkmanager.ProxyId.hostname'
yield 'qutebrowser.command.command.ArgInfo._validate_exclusive'
diff --git a/scripts/dev/ua_fetch.py b/scripts/dev/ua_fetch.py
new file mode 100644
index 000000000..a4ef889a0
--- /dev/null
+++ b/scripts/dev/ua_fetch.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+"""Fetch and print the most common user agents.
+
+This script fetches the most common user agents according to
+https://github.com/Kikobeats/top-user-agents, and prints the most recent
+Chrome user agent for Windows, macOS and Linux.
+"""
+
+import math
+import sys
+import textwrap
+
+import requests
+import qutebrowser.config.websettings
+
+
+def version(ua):
+ """Comparable version of a user agent."""
+ return tuple(int(v) for v in ua.upstream_browser_version.split('.')[:2])
+
+
+def wrap(ini, sub, string):
+ return textwrap.wrap(string, width=80, initial_indent=ini, subsequent_indent=sub)
+
+
+response = requests.get('https://raw.githubusercontent.com/Kikobeats/top-user-agents/master/index.json')
+
+if response.status_code != 200:
+ print('Unable to fetch the user agent index', file=sys.stderr)
+ sys.exit(1)
+
+ua_checks = {
+ 'Win10': lambda ua: ua.os_info.startswith('Windows NT'),
+ 'macOS': lambda ua: ua.os_info.startswith('Macintosh'),
+ 'Linux': lambda ua: ua.os_info.startswith('X11'),
+}
+
+ua_strings = {}
+ua_versions = {}
+ua_names = {}
+
+for ua_string in reversed(response.json()):
+ # reversed to prefer more common versions
+
+ # Filter out browsers that are not Chrome-based
+ parts = ua_string.split()
+ if not any(part.startswith("Chrome/") for part in parts):
+ continue
+ if any(part.startswith("OPR/") or part.startswith("Edg/") for part in parts):
+ continue
+
+ user_agent = qutebrowser.config.websettings.UserAgent.parse(ua_string)
+
+ # check which os_string conditions are met and select the most recent version
+ for key, check in ua_checks.items():
+ if check(user_agent):
+ v = version(user_agent)
+ if v >= ua_versions.get(key, (-math.inf,)):
+ ua_versions[key] = v
+ ua_strings[key] = ua_string
+ ua_names[key] = f'Chrome {v[0]} {key}'
+
+for key, ua_string in ua_strings.items():
+ quoted_ua_string = f'"{ua_string}"'
+ for line in wrap(" - - ", " ", quoted_ua_string):
+ print(line)
+ for line in wrap(" - ", " ", ua_names[key]):
+ print(line)
diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py
index e86ff257d..5f7df5cae 100644
--- a/scripts/dev/update_version.py
+++ b/scripts/dev/update_version.py
@@ -78,13 +78,12 @@ if __name__ == "__main__":
print("* Create new release via GitHub (required to upload release "
"artifacts)")
print("* Linux: git fetch && git checkout v{v} && "
- "./.venv/bin/python3 scripts/dev/build_release.py --upload"
+ "tox -e build-release -- --upload"
.format(v=version))
print("* Windows: git fetch; git checkout v{v}; "
- "py -3 scripts\\dev\\build_release.py --asciidoc "
- "C:\\Python27\\python "
- "$env:userprofile\\bin\\asciidoc-8.6.10\\asciidoc.py --upload"
+ "py -3.7 -m tox -e build-release -- --asciidoc "
+ "$env:userprofile\\bin\\asciidoc-9.0.2\\asciidoc.py --upload"
.format(v=version))
print("* macOS: git fetch && git checkout v{v} && "
- "python3 scripts/dev/build_release.py --upload"
+ "tox -e build-release -- --upload"
.format(v=version))
diff --git a/scripts/dictcli.py b/scripts/dictcli.py
index ebe4e285c..4e38727dd 100755
--- a/scripts/dictcli.py
+++ b/scripts/dictcli.py
@@ -37,8 +37,7 @@ import attr
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from qutebrowser.browser.webengine import spell
from qutebrowser.config import configdata
-from qutebrowser.utils import standarddir, utils
-from scripts import utils as scriptutils
+from qutebrowser.utils import standarddir
API_URL = 'https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries.git/+/master/'
@@ -216,17 +215,8 @@ def install_lang(lang):
def install(languages):
"""Install languages."""
for lang in languages:
- try:
- print('Installing {}: {}'.format(lang.code, lang.name))
- install_lang(lang)
- except PermissionError as e:
- msg = ("\n{}\n\nWith Qt < 5.10, you will need to run this script "
- "as root, as dictionaries need to be installed "
- "system-wide. If your qutebrowser uses a newer Qt version "
- "via a virtualenv, make sure you start this script with "
- "the virtualenv's Python.".format(e))
- scriptutils.print_error(msg)
- sys.exit(1)
+ print('Installing {}: {}'.format(lang.code, lang.name))
+ install_lang(lang)
def update(languages):
@@ -250,24 +240,7 @@ def remove_old(languages):
os.remove(os.path.join(spell.dictionary_dir(), old_file))
-def check_root():
- """Ask for confirmation if running as root when unnecessary."""
- if not utils.is_posix:
- return
-
- if spell.can_use_data_path() and os.geteuid() == 0:
- print("You're running Qt >= 5.10 which means qutebrowser will "
- "load dictionaries from a path in your home-directory. "
- "Unless you run qutebrowser as root (bad idea!), you "
- "most likely want to run this script as your user. ")
- answer = input("Do you want to continue anyways? [y/N] ")
- if answer not in ['y', 'Y']:
- sys.exit(0)
-
-
def main():
- check_root()
-
if configdata.DATA is None:
configdata.init()
standarddir.init(None)
diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py
index 5eeb90640..da2f259e1 100644
--- a/scripts/mkvenv.py
+++ b/scripts/mkvenv.py
@@ -24,12 +24,13 @@
import argparse
import pathlib
import sys
+import re
import os
import os.path
-import typing
import shutil
-import venv
+import venv as pyvenv
import subprocess
+from typing import List, Optional, Tuple, Dict, Union
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils, link_pyqt
@@ -38,7 +39,22 @@ from scripts import utils, link_pyqt
REPO_ROOT = pathlib.Path(__file__).parent.parent
-def parse_args() -> argparse.Namespace:
+class Error(Exception):
+
+ """Exception for errors in this script."""
+
+ def __init__(self, msg, code=1):
+ super().__init__(msg)
+ self.code = code
+
+
+def print_command(*cmd: Union[str, pathlib.Path], venv: bool) -> None:
+ """Print a command being run."""
+ prefix = 'venv$ ' if venv else '$ '
+ utils.print_col(prefix + ' '.join([str(e) for e in cmd]), 'blue')
+
+
+def parse_args(argv: List[str] = None) -> argparse.Namespace:
"""Parse commandline arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--keep',
@@ -71,13 +87,16 @@ def parse_args() -> argparse.Namespace:
parser.add_argument('--skip-docs',
action='store_true',
help="Skip doc generation.")
+ parser.add_argument('--skip-smoke-test',
+ action='store_true',
+ help="Skip Qt smoke test.")
parser.add_argument('--tox-error',
action='store_true',
help=argparse.SUPPRESS)
- return parser.parse_args()
+ return parser.parse_args(argv)
-def pyqt_versions() -> typing.List[str]:
+def pyqt_versions() -> List[str]:
"""Get a list of all available PyQt versions.
The list is based on the filenames of misc/requirements/ files.
@@ -93,22 +112,40 @@ def pyqt_versions() -> typing.List[str]:
return versions + ['auto']
-def run_venv(venv_dir: pathlib.Path, executable, *args: str) -> None:
+def run_venv(
+ venv_dir: pathlib.Path,
+ executable,
+ *args: str,
+ capture_output=False,
+ capture_error=False,
+ env=None,
+) -> subprocess.CompletedProcess:
"""Run the given command inside the virtualenv."""
subdir = 'Scripts' if os.name == 'nt' else 'bin'
+ if env is None:
+ proc_env = None
+ else:
+ proc_env = os.environ.copy()
+ proc_env.update(env)
+
try:
- subprocess.run([str(venv_dir / subdir / executable)] +
- [str(arg) for arg in args], check=True)
+ return subprocess.run(
+ [str(venv_dir / subdir / executable)] + [str(arg) for arg in args],
+ check=True,
+ universal_newlines=capture_output or capture_error,
+ stdout=subprocess.PIPE if capture_output else None,
+ stderr=subprocess.PIPE if capture_error else None,
+ env=proc_env,
+ )
except subprocess.CalledProcessError as e:
- utils.print_error("Subprocess failed, exiting")
- sys.exit(e.returncode)
+ raise Error("Subprocess failed, exiting") from e
def pip_install(venv_dir: pathlib.Path, *args: str) -> None:
"""Run a pip install command inside the virtualenv."""
arg_str = ' '.join(str(arg) for arg in args)
- utils.print_col('venv$ pip install {}'.format(arg_str), 'blue')
+ print_command('pip install', arg_str, venv=True)
run_venv(venv_dir, 'python', '-m', 'pip', 'install', *args)
@@ -125,27 +162,25 @@ def delete_old_venv(venv_dir: pathlib.Path) -> None:
]
if not any(m.exists() for m in markers):
- utils.print_error('{} does not look like a virtualenv, '
- 'cowardly refusing to remove it.'.format(venv_dir))
- sys.exit(1)
+ raise Error('{} does not look like a virtualenv, cowardly refusing to '
+ 'remove it.'.format(venv_dir))
- utils.print_col('$ rm -r {}'.format(venv_dir), 'blue')
+ print_command('rm -r', venv_dir, venv=False)
shutil.rmtree(str(venv_dir))
def create_venv(venv_dir: pathlib.Path, use_virtualenv: bool = False) -> None:
"""Create a new virtualenv."""
if use_virtualenv:
- utils.print_col('$ python3 -m virtualenv {}'.format(venv_dir), 'blue')
+ print_command('python3 -m virtualenv', venv_dir, venv=False)
try:
subprocess.run([sys.executable, '-m', 'virtualenv', venv_dir],
check=True)
except subprocess.CalledProcessError as e:
- utils.print_error("virtualenv failed, exiting")
- sys.exit(e.returncode)
+ raise Error("virtualenv failed, exiting", e.returncode)
else:
- utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue')
- venv.create(str(venv_dir), with_pip=True)
+ print_command('python3 -m venv', venv_dir, venv=False)
+ pyvenv.create(str(venv_dir), with_pip=True)
def upgrade_seed_pkgs(venv_dir: pathlib.Path) -> None:
@@ -202,6 +237,129 @@ def install_pyqt_wheels(venv_dir: pathlib.Path,
pip_install(venv_dir, *wheels)
+def apply_xcb_util_workaround(
+ venv_dir: pathlib.Path,
+ pyqt_type: str,
+ pyqt_version: str,
+) -> None:
+ """If needed (Debian Stable), symlink libxcb-util.so.0 -> .1.
+
+ WORKAROUND for https://bugreports.qt.io/browse/QTBUG-88688
+ """
+ utils.print_title("Running xcb-util workaround")
+
+ if not sys.platform.startswith('linux'):
+ print("Workaround not needed: Not on Linux.")
+ return
+ if pyqt_type != 'binary':
+ print("Workaround not needed: Not installing from PyQt binaries.")
+ return
+ if pyqt_version not in ['auto', '5.15']:
+ print("Workaround not needed: Not installing Qt 5.15.")
+ return
+
+ libs = _find_libs()
+ abi_type = 'libc6,x86-64' # the only one PyQt wheels are available for
+
+ if ('libxcb-util.so.1', abi_type) in libs:
+ print("Workaround not needed: libxcb-util.so.1 found.")
+ return
+
+ try:
+ libxcb_util_libs = libs['libxcb-util.so.0', abi_type]
+ except KeyError:
+ utils.print_error('Workaround failed: libxcb-util.so.0 not found.')
+ return
+
+ if len(libxcb_util_libs) > 1:
+ utils.print_error(
+ f'Workaround failed: Multiple matching libxcb-util found: '
+ f'{libxcb_util_libs}')
+ return
+
+ libxcb_util_path = pathlib.Path(libxcb_util_libs[0])
+
+ code = [
+ 'from PyQt5.QtCore import QLibraryInfo',
+ 'print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))',
+ ]
+ proc = run_venv(venv_dir, 'python', '-c', '; '.join(code), capture_output=True)
+ venv_lib_path = pathlib.Path(proc.stdout.strip())
+
+ link_path = venv_lib_path / libxcb_util_path.with_suffix('.1').name
+
+ # This gives us a nicer path to print, and also conveniently makes sure we
+ # didn't accidentally end up with a path outside the venv.
+ rel_link_path = venv_dir / link_path.relative_to(venv_dir.resolve())
+ print_command('ln -s', libxcb_util_path, rel_link_path, venv=False)
+
+ link_path.symlink_to(libxcb_util_path)
+
+
+def _find_libs() -> Dict[Tuple[str, str], List[str]]:
+ """Find all system-wide .so libraries."""
+ all_libs: Dict[Tuple[str, str], List[str]] = {}
+
+ if pathlib.Path("/sbin/ldconfig").exists():
+ # /sbin might not be in PATH on e.g. Debian
+ ldconfig_bin = "/sbin/ldconfig"
+ else:
+ ldconfig_bin = "ldconfig"
+ ldconfig_proc = subprocess.run(
+ [ldconfig_bin, '-p'],
+ check=True,
+ stdout=subprocess.PIPE,
+ encoding=sys.getfilesystemencoding(),
+ )
+
+ pattern = re.compile(r'(?P<name>\S+) \((?P<abi_type>[^)]+)\) => (?P<path>.*)')
+ for line in ldconfig_proc.stdout.splitlines():
+ match = pattern.fullmatch(line.strip())
+ if match is None:
+ if 'libs found in cache' not in line:
+ utils.print_col(f'Failed to match ldconfig output: {line}', 'yellow')
+ continue
+
+ key = match.group('name'), match.group('abi_type')
+ path = match.group('path')
+
+ libs = all_libs.setdefault(key, [])
+ libs.append(path)
+
+ return all_libs
+
+
+def run_qt_smoke_test(venv_dir: pathlib.Path) -> None:
+ """Make sure the Qt installation works."""
+ utils.print_title("Running Qt smoke test")
+ code = [
+ 'import sys',
+ 'from PyQt5.QtWidgets import QApplication',
+ 'from PyQt5.QtCore import qVersion, QT_VERSION_STR, PYQT_VERSION_STR',
+ 'print(f"Python: {sys.version}")',
+ 'print(f"qVersion: {qVersion()}")',
+ 'print(f"QT_VERSION_STR: {QT_VERSION_STR}")',
+ 'print(f"PYQT_VERSION_STR: {PYQT_VERSION_STR}")',
+ 'QApplication([])',
+ 'print("Qt seems to work properly!")',
+ 'print()',
+ ]
+ try:
+ run_venv(
+ venv_dir,
+ 'python', '-c', '; '.join(code),
+ env={'QT_DEBUG_PLUGINS': '1'},
+ capture_error=True
+ )
+ except Error as e:
+ proc_e = e.__cause__
+ assert isinstance(proc_e, subprocess.CalledProcessError), proc_e
+ print(proc_e.stderr)
+ raise Error(
+ f"Smoke test failed with status {proc_e.returncode}. "
+ "You might find additional information in the debug output above.")
+
+
def install_requirements(venv_dir: pathlib.Path) -> None:
"""Install qutebrowser's requirement.txt."""
utils.print_title("Installing other qutebrowser dependencies")
@@ -224,7 +382,7 @@ def install_qutebrowser(venv_dir: pathlib.Path) -> None:
def regenerate_docs(venv_dir: pathlib.Path,
- asciidoc: typing.Optional[typing.Tuple[str, str]]):
+ asciidoc: Optional[Tuple[str, str]]):
"""Regenerate docs using asciidoc."""
utils.print_title("Generating documentation")
if asciidoc is not None:
@@ -233,27 +391,24 @@ def regenerate_docs(venv_dir: pathlib.Path,
a2h_args = []
script_path = pathlib.Path(__file__).parent / 'asciidoc2html.py'
- utils.print_col('venv$ python3 scripts/asciidoc2html.py {}'
- .format(' '.join(a2h_args)), 'blue')
+ print_command('python3 scripts/asciidoc2html.py', *a2h_args, venv=True)
run_venv(venv_dir, 'python', str(script_path), *a2h_args)
-def main() -> None:
+def run(args) -> None:
"""Install qutebrowser in a virtualenv.."""
- args = parse_args()
venv_dir = pathlib.Path(args.venv_dir)
wheels_dir = pathlib.Path(args.pyqt_wheels_dir)
utils.change_cwd()
if (args.pyqt_version != 'auto' and
args.pyqt_type not in ['binary', 'source']):
- utils.print_error('The --pyqt-version option is only available when '
- 'installing PyQt from binary or source')
- sys.exit(1)
- elif args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels':
- utils.print_error('The --pyqt-wheels-dir option is only available '
- 'when installing PyQt from wheels')
- sys.exit(1)
+ raise Error('The --pyqt-version option is only available when installing PyQt '
+ 'from binary or source')
+
+ if args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels':
+ raise Error('The --pyqt-wheels-dir option is only available when installing '
+ 'PyQt from wheels')
if not args.keep:
utils.print_title("Creating virtual environment")
@@ -275,6 +430,10 @@ def main() -> None:
else:
raise AssertionError
+ apply_xcb_util_workaround(venv_dir, args.pyqt_type, args.pyqt_version)
+ if args.pyqt_type != 'skip':
+ run_qt_smoke_test(venv_dir)
+
install_requirements(venv_dir)
install_qutebrowser(venv_dir)
if args.dev:
@@ -284,5 +443,14 @@ def main() -> None:
regenerate_docs(venv_dir, args.asciidoc)
+def main():
+ args = parse_args()
+ try:
+ run(args)
+ except Error as e:
+ utils.print_error(str(e))
+ sys.exit(e.code)
+
+
if __name__ == '__main__':
main()
diff --git a/scripts/open_url_in_instance.sh b/scripts/open_url_in_instance.sh
index ec2a5a26d..0d6edef51 100755
--- a/scripts/open_url_in_instance.sh
+++ b/scripts/open_url_in_instance.sh
@@ -12,4 +12,4 @@ printf '{"args": ["%s"], "target_arg": null, "version": "%s", "protocol_version"
"${_url}" \
"${_qb_version}" \
"${_proto_version}" \
- "${PWD}" | socat - UNIX-CONNECT:"${_ipc_socket}" 2>/dev/null || "$_qute_bin" "$@" &
+ "${PWD}" | socat -lf /dev/null - UNIX-CONNECT:"${_ipc_socket}" || "$_qute_bin" "$@" &
diff --git a/setup.py b/setup.py
index 0c0bf73b4..1169eae81 100755
--- a/setup.py
+++ b/setup.py
@@ -72,7 +72,7 @@ try:
['qutebrowser = qutebrowser.qutebrowser:main']},
zip_safe=True,
install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
- python_requires='>=3.5',
+ python_requires='>=3.6',
name='qutebrowser',
version=_get_constant('version'),
description=_get_constant('description'),
@@ -94,7 +94,6 @@ try:
'Operating System :: MacOS',
'Operating System :: POSIX :: BSD',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
diff --git a/tests/conftest.py b/tests/conftest.py
index d4d06c6bc..017c11ba8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -34,7 +34,7 @@ pytest.register_assert_rewrite('helpers')
from helpers import logfail
from helpers.logfail import fail_on_logging
-from helpers.messagemock import message_mock, message_bridge
+from helpers.messagemock import message_mock
from helpers.fixtures import * # noqa: F403
from helpers import utils as testutils
from qutebrowser.utils import qtutils, standarddir, usertypes, utils, version
@@ -101,13 +101,6 @@ def _apply_platform_markers(config, item):
sys.getfilesystemencoding() == 'ascii',
"Skipped because of ASCII locale"),
- ('qtbug60673',
- pytest.mark.xfail,
- qtutils.version_check('5.8') and
- not qtutils.version_check('5.10') and
- config.webengine,
- "Broken on webengine due to "
- "https://bugreports.qt.io/browse/QTBUG-60673"),
('qtwebkit6021_xfail',
pytest.mark.xfail,
version.qWebKitVersion and # type: ignore[unreachable]
@@ -175,11 +168,6 @@ def pytest_collection_modifyitems(config, items):
_apply_platform_markers(config, item)
if list(item.iter_markers('xfail_norun')):
item.add_marker(pytest.mark.xfail(run=False))
- if list(item.iter_markers('js_prompt')):
- if config.webengine:
- item.add_marker(pytest.mark.skipif(
- PYQT_VERSION <= 0x050700,
- reason='JS prompts are not supported with PyQt 5.7'))
if deselected:
deselected_items.append(item)
@@ -281,10 +269,10 @@ def apply_fake_os(monkeypatch, request):
else:
raise ValueError("Invalid fake_os {}".format(name))
- monkeypatch.setattr('qutebrowser.utils.utils.is_mac', mac)
- monkeypatch.setattr('qutebrowser.utils.utils.is_linux', linux)
- monkeypatch.setattr('qutebrowser.utils.utils.is_windows', windows)
- monkeypatch.setattr('qutebrowser.utils.utils.is_posix', posix)
+ monkeypatch.setattr(utils, 'is_mac', mac)
+ monkeypatch.setattr(utils, 'is_linux', linux)
+ monkeypatch.setattr(utils, 'is_windows', windows)
+ monkeypatch.setattr(utils, 'is_posix', posix)
@pytest.fixture(scope='session', autouse=True)
diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py
index 43105d6cb..17b457521 100644
--- a/tests/end2end/conftest.py
+++ b/tests/end2end/conftest.py
@@ -35,7 +35,7 @@ from PyQt5.QtCore import PYQT_VERSION, QCoreApplication
pytest.register_assert_rewrite('end2end.fixtures')
-from end2end.fixtures.webserver import server, server_per_test, ssl_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
@@ -140,8 +140,7 @@ def pytest_collection_modifyitems(config, items):
"""Apply @qtwebengine_* markers; skip unittests with QUTE_BDD_WEBENGINE."""
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884
# (note this isn't actually fixed properly before Qt 5.15)
- header_bug_fixed = (not qtutils.version_check('5.12', compiled=False) or
- qtutils.version_check('5.15', compiled=False))
+ header_bug_fixed = qtutils.version_check('5.15', compiled=False)
lib_path = pathlib.Path(QCoreApplication.libraryPaths()[0])
qpdf_image_plugin = lib_path / 'imageformats' / 'libqpdf.so'
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py
index 0208cce05..87748a43a 100644
--- a/tests/end2end/features/conftest.py
+++ b/tests/end2end/features/conftest.py
@@ -378,6 +378,7 @@ def hint(quteproc, args):
@bdd.when(bdd.parsers.parse('I hint with args "{args}" and follow {letter}'))
def hint_and_follow(quteproc, args, letter):
args = args.replace('(testdata)', testutils.abs_datapath())
+ args = args.replace('(python-executable)', sys.executable)
quteproc.send_cmd(':hint {}'.format(args))
quteproc.wait_for(message='hints: *')
quteproc.send_cmd(':follow-hint {}'.format(letter))
diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature
index a440590b8..df2e10b46 100644
--- a/tests/end2end/features/downloads.feature
+++ b/tests/end2end/features/downloads.feature
@@ -101,18 +101,6 @@ Feature: Downloading things from a website.
And I run :leave-mode
Then no crash should happen
- # https://github.com/qutebrowser/qutebrowser/issues/4240
- @qt<5.11.2
- Scenario: Downloading with SSL errors (issue 1413)
- When SSL is supported
- And I clear SSL errors
- And I set content.ssl_strict to ask
- And I set downloads.location.prompt to false
- And I download an SSL page
- And I wait for "Entering mode KeyMode.* (reason: question asked)" in the log
- And I run :prompt-accept
- Then the error "Download error: SSL handshake failed" should be shown
-
Scenario: Closing window with downloads.remove_finished timeout (issue 1242)
When I set downloads.remove_finished to 500
And I open data/downloads/download.bin in a new window without waiting
@@ -148,16 +136,16 @@ Feature: Downloading things from a website.
And I wait until the download is finished
Then the downloaded file download with spaces.bin should exist
- @qtwebkit_skip @qt<5.9 @qt>=5.13
- Scenario: Downloading a file with evil content-disposition header (Qt 5.8 or older and 5.13 and newer)
+ @qtwebkit_skip @qt>=5.13
+ Scenario: Downloading a file with evil content-disposition header (Qt 5.13 and newer)
# Content-Disposition: download; filename=..%2Ffoo
When I open response-headers?Content-Disposition=download;%20filename%3D..%252Ffoo without waiting
And I wait until the download is finished
Then the downloaded file ../foo should not exist
And the downloaded file foo should exist
- @qtwebkit_skip @qt<5.13 @qt>=5.9
- Scenario: Downloading a file with evil content-disposition header (Qt 5.9 to 5.12)
+ @qtwebkit_skip @qt<5.13
+ Scenario: Downloading a file with evil content-disposition header (Qt 5.12)
# Content-Disposition: download; filename=..%2Ffoo
When I open response-headers?Content-Disposition=download;%20filename%3D..%252Ffoo without waiting
And I wait until the download is finished
@@ -211,24 +199,6 @@ Feature: Downloading things from a website.
does-not-exist
does-not-exist
- @qtwebkit_skip @qt<5.10
- Scenario: Retrying a failed download with QtWebEngine (Qt < 5.10)
- When I open data/downloads/issue2298.html
- And I run :click-element id download
- And I wait for "Download error: *" in the log
- And I run :download-retry
- Then the error "Retrying downloads is unsupported *" should be shown
-
- @qtwebkit_skip @qt==5.10.1
- Scenario: Retrying a failed download with QtWebEngine (Qt 5.10)
- When I open data/downloads/issue2298.html
- And I run :click-element id download
- And I wait for "Download error: *" in the log
- And I run :download-retry
- # For some reason it doesn't actually try again here, but let's hope it
- # works e.g. on a connection loss, which we can't test automatically.
- Then "Retrying downloads is unsupported *" should not be logged
-
@flaky
Scenario: Retrying with count
When I run :download http://localhost:(port)/data/downloads/download.bin
diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature
index caf1200e2..271450d90 100644
--- a/tests/end2end/features/hints.feature
+++ b/tests/end2end/features/hints.feature
@@ -61,17 +61,17 @@ Feature: Using hints
Scenario: Using :hint spawn with flags and -- (issue 797)
When I open data/hints/html/simple.html
- And I hint with args "-- all spawn -v python -c ''" and follow a
+ And I hint with args "-- all spawn -v (python-executable) -c ''" and follow a
Then the message "Command exited successfully." should be shown
Scenario: Using :hint spawn with flags (issue 797)
When I open data/hints/html/simple.html
- And I hint with args "all spawn -v python -c ''" and follow a
+ And I hint with args "all spawn -v (python-executable) -c ''" and follow a
Then the message "Command exited successfully." should be shown
Scenario: Using :hint spawn with flags and --rapid (issue 797)
When I open data/hints/html/simple.html
- And I hint with args "--rapid all spawn -v python -c ''" and follow a
+ And I hint with args "--rapid all spawn -v (python-executable) -c ''" and follow a
Then the message "Command exited successfully." should be shown
@posix
@@ -179,18 +179,6 @@ Feature: Using hints
And I hint with args "all run message-info {hint-url}" and follow a
Then the message "http://localhost:(port)/data/hello.txt" should be shown
- @qt<5.11
- Scenario: Clicking an invalid link
- When I open data/invalid_link.html
- And I hint with args "all" and follow a
- Then the error "Invalid link clicked - *" should be shown
-
- @qt<5.11
- Scenario: Clicking an invalid link opening in a new tab
- When I open data/invalid_link.html
- And I hint with args "all tab" and follow a
- Then the error "Invalid link clicked - *" should be shown
-
Scenario: Hinting inputs without type
When I open data/hints/input.html
And I hint with args "inputs" and follow a
diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature
index a4c3d1338..6238e8b7b 100644
--- a/tests/end2end/features/javascript.feature
+++ b/tests/end2end/features/javascript.feature
@@ -124,33 +124,11 @@ Feature: Javascript stuff
# https://github.com/qutebrowser/qutebrowser/issues/1190
# https://github.com/qutebrowser/qutebrowser/issues/2495
- # Currently broken on Windows and on Qt 5.12
- # https://github.com/qutebrowser/qutebrowser/issues/4230
- @posix @qt<5.12
- Scenario: Checking visible/invisible window size
- When I run :tab-only
- And I open data/javascript/windowsize.html in a new background tab
- And I wait for "[*/data/javascript/windowsize.html:*] loaded" in the log
- And I run :tab-next
- Then the window sizes should be the same
-
- @flaky @qt<5.12
- Scenario: Checking visible/invisible window size with vertical tabbar
- When I run :tab-only
- And I set tabs.position to left
- And I open data/javascript/windowsize.html in a new background tab
- And I wait for "[*/data/javascript/windowsize.html:*] loaded" in the log
- And I run :tab-next
- Then the window sizes should be the same
-
@flaky
Scenario: Have a GreaseMonkey script run at page start
When I have a GreaseMonkey file saved for document-start with noframes unset
And I run :greasemonkey-reload
And I open data/hints/iframe.html
- # This second reload is required in webengine < 5.8 for scripts
- # registered to run at document-start, some sort of timing issue.
- And I run :reload
Then the javascript message "Script is running on /data/hints/iframe.html" should be logged
Scenario: Have a GreaseMonkey script running on frames
diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature
index 2769e3dc3..aa9009f7c 100644
--- a/tests/end2end/features/keyinput.feature
+++ b/tests/end2end/features/keyinput.feature
@@ -18,31 +18,6 @@ Feature: Keyboard input
# input.forward_unbound_keys
- @qt<5.11.1
- Scenario: Forwarding all keys
- When I open data/keyinput/log.html
- And I set input.forward_unbound_keys to all
- And I press the key ","
- And I press the key "<F1>"
- # ,
- Then the javascript message "key press: 188" should be logged
- And the javascript message "key release: 188" should be logged
- # <F1>
- And the javascript message "key press: 112" should be logged
- And the javascript message "key release: 112" should be logged
-
- @qt<5.11.1
- Scenario: Forwarding special keys
- When I open data/keyinput/log.html
- And I set input.forward_unbound_keys to auto
- And I press the keys ",<F1>"
- # <F1>
- Then the javascript message "key press: 112" should be logged
- And the javascript message "key release: 112" should be logged
- # ,
- And the javascript message "key press: 188" should not be logged
- And the javascript message "key release: 188" should not be logged
-
Scenario: Forwarding no keys
When I open data/keyinput/log.html
And I set input.forward_unbound_keys to none
@@ -66,10 +41,10 @@ Feature: Keyboard input
@no_xvfb @posix @qtwebengine_skip
Scenario: :fake-key sending key to the website with other window focused
When I open data/keyinput/log.html
- And I run :inspector
+ And I run :devtools
And I wait for "Focus object changed: <PyQt5.QtWebKitWidgets.QWebView object at *>" in the log
And I run :fake-key x
- And I run :inspector
+ And I run :devtools
And I wait for "Focus object changed: <qutebrowser.browser.webkit.webview.WebView *>" in the log
Then the error "No focused webview!" should be shown
diff --git a/tests/end2end/features/marks.feature b/tests/end2end/features/marks.feature
index 7ac60edeb..921e0e76c 100644
--- a/tests/end2end/features/marks.feature
+++ b/tests/end2end/features/marks.feature
@@ -112,11 +112,11 @@ Feature: Setting positional marks
Then the page should be scrolled to 0 0
Scenario: Hovering a hint does not set the ' mark
- When I run :scroll-px 30 20
- And I wait until the scroll position changed to 30/20
- And I run :scroll-to-perc 0
+ When I run :scroll-px 10 20
+ And I wait until the scroll position changed to 10/20
+ And I run :scroll-to-perc 0
And I wait until the scroll position changed
And I hint with args "links hover" and follow s
And I run :jump-mark "'"
- And I wait until the scroll position changed to 30/20
- Then the page should be scrolled to 30 20
+ And I wait until the scroll position changed to 10/20
+ Then the page should be scrolled to 10 20
diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature
index 93a15cd62..06dc0b805 100644
--- a/tests/end2end/features/misc.feature
+++ b/tests/end2end/features/misc.feature
@@ -171,25 +171,20 @@ Feature: Various utility commands.
# :inspect
- @qtwebkit_skip @qt<5.11
- Scenario: Inspector without --enable-webengine-inspector
- When I run :devtools
- Then the error "QtWebEngine inspector is not enabled. See 'qutebrowser --help' for details." should be shown
-
@no_xvfb @posix @qtwebengine_skip
Scenario: Inspector smoke test
- When I run :inspector
+ When I run :devtools
And I wait for "Focus object changed: <PyQt5.QtWebKitWidgets.QWebView object at *>" in the log
- And I run :inspector
+ And I run :devtools
And I wait for "Focus object changed: *" in the log
Then no crash should happen
# Different code path as an inspector got created now
@no_xvfb @posix @qtwebengine_skip
Scenario: Inspector smoke test 2
- When I run :inspector
+ When I run :devtools
And I wait for "Focus object changed: <PyQt5.QtWebKitWidgets.QWebView object at *>" in the log
- And I run :inspector
+ And I run :devtools
And I wait for "Focus object changed: *" in the log
Then no crash should happen
@@ -314,7 +309,6 @@ Feature: Various utility commands.
And I press the key "<Ctrl-C>"
Then no crash should happen
- @js_prompt
Scenario: Focusing download widget via Tab (original issue)
When I open data/prompt/jsprompt.html
And I run :click-element id button
@@ -506,25 +500,14 @@ Feature: Various utility commands.
## Renderer crashes
# Skipped on Windows as "... has stopped working" hangs.
- @qtwebkit_skip @no_invalid_lines @posix @qt<5.9
+ @qtwebkit_skip @no_invalid_lines @posix
Scenario: Renderer crash
When I run :open -t chrome://crash
- Then the error "Renderer process crashed" should be shown
-
- @qtwebkit_skip @no_invalid_lines @qt<5.9
- Scenario: Renderer kill
- When I run :open -t chrome://kill
- Then the error "Renderer process was killed" should be shown
-
- # Skipped on Windows as "... has stopped working" hangs.
- @qtwebkit_skip @no_invalid_lines @posix @qt>=5.9
- Scenario: Renderer crash (5.9)
- When I run :open -t chrome://crash
Then "Renderer process crashed" should be logged
And "* 'Error loading chrome://crash/'" should be logged
- @qtwebkit_skip @no_invalid_lines @qt>=5.9 @flaky
- Scenario: Renderer kill (5.9)
+ @qtwebkit_skip @no_invalid_lines @flaky
+ Scenario: Renderer kill
When I run :open -t chrome://kill
Then "Renderer process was killed" should be logged
And "* 'Error loading chrome://kill/'" should be logged
diff --git a/tests/end2end/features/private.feature b/tests/end2end/features/private.feature
index 07ff225a3..fe870dded 100644
--- a/tests/end2end/features/private.feature
+++ b/tests/end2end/features/private.feature
@@ -172,7 +172,7 @@ Feature: Using private browsing
- url: http://localhost:*/data/numbers/1.txt
- url: http://localhost:*/data/numbers/2.txt
- @flaky
+ @skip # Too flaky
Scenario: Saving a private session with only-active-window
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
@@ -181,7 +181,12 @@ Feature: Using private browsing
And I open data/numbers/5.txt in a new tab
And I run :session-save --only-active-window window_session_name
And I run :window-only
+ And I wait for "removed: tab" in the log
+ And I wait for "removed: tab" in the log
And I run :tab-only
+ And I wait for "removed: tab" in the log
+ And I wait for "removed: tab" in the log
+ And I wait for "removed: tab" in the log
And I run :session-load -c window_session_name
And I wait until data/numbers/5.txt is loaded
Then the session should look like:
diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature
index 96532dc8c..a0b550054 100644
--- a/tests/end2end/features/prompts.feature
+++ b/tests/end2end/features/prompts.feature
@@ -39,7 +39,6 @@ Feature: Prompts
And I run :leave-mode
Then the javascript message "confirm reply: false" should be logged
- @js_prompt
Scenario: Javascript prompt
When I open data/prompt/jsprompt.html
And I run :click-element id button
@@ -48,7 +47,6 @@ Feature: Prompts
And I run :prompt-accept
Then the javascript message "Prompt reply: prompt test" should be logged
- @js_prompt
Scenario: Javascript prompt with default
When I open data/prompt/jsprompt.html
And I run :click-element id button-default
@@ -56,7 +54,6 @@ Feature: Prompts
And I run :prompt-accept
Then the javascript message "Prompt reply: default" should be logged
- @js_prompt
Scenario: Rejected javascript prompt
When I open data/prompt/jsprompt.html
And I run :click-element id button
@@ -137,7 +134,6 @@ Feature: Prompts
# Shift-Insert with prompt (issue 1299)
- @js_prompt
Scenario: Pasting via shift-insert in prompt mode
When selection is supported
And I put "insert test" into the primary selection
@@ -148,7 +144,6 @@ Feature: Prompts
And I run :prompt-accept
Then the javascript message "Prompt reply: insert test" should be logged
- @js_prompt
Scenario: Pasting via shift-insert without it being supported
When selection is not supported
And I put "insert test" into the primary selection
@@ -160,7 +155,6 @@ Feature: Prompts
And I run :prompt-accept
Then the javascript message "Prompt reply: clipboard test" should be logged
- @js_prompt
Scenario: Using content.javascript.prompt
When I set content.javascript.prompt to false
And I open data/prompt/jsprompt.html
@@ -396,7 +390,6 @@ Feature: Prompts
Then the javascript message "Alert done" should be logged
And the error "No value is permitted with alert prompts!" should be shown
- @js_prompt
Scenario: Javascript prompt with value
When I set content.javascript.prompt to true
And I open data/prompt/jsprompt.html
diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature
index 55e366b4f..4c283710a 100644
--- a/tests/end2end/features/qutescheme.feature
+++ b/tests/end2end/features/qutescheme.feature
@@ -89,6 +89,7 @@ Feature: Special qute:// pages
And I open qute://help/img/ without waiting
Then "*Error while * qute://*" should be logged
And "* url='qute://help/img'* LoadStatus.error" should be logged
+ And "Load error: ERR_FILE_NOT_FOUND" should be logged
# :history
@@ -184,7 +185,6 @@ Feature: Special qute:// pages
And I open data/misc/test.pdf without waiting
Then "Download test.pdf finished" should be logged
- @qtwebengine_skip: Might work with Qt 5.12
Scenario: Downloading a pdf via pdf.js button (issue 1214)
Given pdfjs is available
When I set content.pdfjs to true
@@ -192,7 +192,7 @@ Feature: Special qute:// pages
And I open data/misc/test.pdf without waiting
And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log
And I run :jseval document.getElementById("download").click()
- And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
+ And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
And I run :leave-mode
Then no crash should happen
@@ -282,7 +282,6 @@ Feature: Special qute:// pages
Scenario: Using qute://log directly
When I open qute://log without waiting
- # With Qt 5.9, we don't get a loaded message?
And I wait for "Changing title for idx * to 'log'" in the log
Then no crash should happen
diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature
index 4b645d554..75051ad44 100644
--- a/tests/end2end/features/tabs.feature
+++ b/tests/end2end/features/tabs.feature
@@ -737,15 +737,8 @@ Feature: Tab management
# https://github.com/qutebrowser/qutebrowser/issues/2289
- @qtwebkit_skip @qt<5.9
- Scenario: Cloning a tab with a view-source URL
- When I open /
- And I open view-source:http://localhost:(port)
- And I run :tab-clone
- Then the error "Can't serialize special URL!" should be shown
-
- @qtwebkit_skip @qt>=5.9
- Scenario: Cloning a tab with a special URL (Qt 5.9)
+ @qtwebkit_skip
+ Scenario: Cloning a tab with a special URL
When I open chrome://gpu
And I run :tab-clone
Then no crash should happen
@@ -1613,3 +1606,12 @@ Feature: Tab management
And I open data/hello.txt in a new tab
And I run :fake-key -g hello-world<enter>
Then the message "hello-world" should be shown
+
+ Scenario: Undo after changing tabs_are_windows
+ When I open data/hello.txt
+ And I open data/hello.txt in a new tab
+ And I set tabs.tabs_are_windows to true
+ And I run :tab-close
+ And I run :undo
+ And I run :message-info "Still alive!"
+ Then the message "Still alive!" should be shown
diff --git a/tests/end2end/features/test_downloads_bdd.py b/tests/end2end/features/test_downloads_bdd.py
index 951ebcfea..1f27d2794 100644
--- a/tests/end2end/features/test_downloads_bdd.py
+++ b/tests/end2end/features/test_downloads_bdd.py
@@ -96,12 +96,6 @@ def wait_for_download_prompt(tmpdir, quteproc, path):
"(reason: question asked)")
-@bdd.when("I download an SSL page")
-def download_ssl_page(quteproc, ssl_server):
- quteproc.send_cmd(':download https://localhost:{}/'
- .format(ssl_server.port))
-
-
@bdd.then(bdd.parsers.parse("The downloaded file {filename} should not exist"))
def download_should_not_exist(filename, tmpdir):
path = tmpdir / 'downloads' / filename
diff --git a/tests/end2end/features/test_prompts_bdd.py b/tests/end2end/features/test_prompts_bdd.py
index 98f2febbb..61cfcd8f3 100644
--- a/tests/end2end/features/test_prompts_bdd.py
+++ b/tests/end2end/features/test_prompts_bdd.py
@@ -22,8 +22,6 @@ import logging
import pytest_bdd as bdd
bdd.scenarios('prompts.feature')
-from qutebrowser.utils import qtutils
-
@bdd.when("I load an SSL page")
def load_ssl_page(quteproc, ssl_server):
@@ -51,24 +49,17 @@ def no_prompt_shown(quteproc):
@bdd.then("a SSL error page should be shown")
def ssl_error_page(request, quteproc):
- if request.config.webengine and qtutils.version_check('5.9'):
+ if request.config.webengine:
quteproc.wait_for(message="Certificate error: *")
msg = quteproc.wait_for(message="Load error: *")
msg.expected = True
- expected_messages = [
- 'Load error: ERR_INSECURE_RESPONSE', # Qt <= 5.10
- 'Load error: ERR_CERT_AUTHORITY_INVALID', # Qt 5.11
- ]
- assert msg.message in expected_messages
+ assert msg.message == 'Load error: ERR_CERT_AUTHORITY_INVALID'
else:
- if not request.config.webengine:
- line = quteproc.wait_for(message='Error while loading *: SSL '
- 'handshake failed')
- line.expected = True
- quteproc.wait_for(message="Changing title for idx * to 'Error "
- "loading page: *'")
+ line = quteproc.wait_for(message='Error while loading *: SSL handshake failed')
+ line.expected = True
+ quteproc.wait_for(message="Changing title for idx * to 'Error loading page: *'")
content = quteproc.get_content().strip()
assert "Unable to load page" in content
diff --git a/tests/end2end/features/test_qutescheme_bdd.py b/tests/end2end/features/test_qutescheme_bdd.py
index 587aadc41..1be913ac9 100644
--- a/tests/end2end/features/test_qutescheme_bdd.py
+++ b/tests/end2end/features/test_qutescheme_bdd.py
@@ -47,7 +47,7 @@ def request_blocked(request, quteproc, kind):
webkit_error_unsupported = (
"Error while loading qute://settings/set?*: Unsupported request type")
- if request.config.webengine and qtutils.version_check('5.12'):
+ if request.config.webengine:
# On Qt 5.12, we mark qute:// as a local scheme, causing most requests
# being blocked by Chromium internally (logging to the JS console).
expected_messages = {
@@ -60,13 +60,6 @@ def request_blocked(request, quteproc, kind):
# On Qt 5.15, Chromium blocks the redirect as ERR_UNSAFE_REDIRECT
# instead.
expected_messages['redirect'] = [unsafe_redirect_msg]
- elif request.config.webengine:
- expected_messages = {
- 'img': [blocking_csrf_msg],
- 'link': [blocking_set_msg, blocked_request_msg],
- 'redirect': [blocking_set_msg, blocked_request_msg],
- 'form': [blocking_set_msg, blocked_request_msg],
- }
else: # QtWebKit
expected_messages = {
'img': [blocking_csrf_msg],
diff --git a/tests/end2end/features/test_search_bdd.py b/tests/end2end/features/test_search_bdd.py
index ba3254830..3a9fb036c 100644
--- a/tests/end2end/features/test_search_bdd.py
+++ b/tests/end2end/features/test_search_bdd.py
@@ -19,11 +19,14 @@
import json
+import pytest
import pytest_bdd as bdd
-# pylint: disable=unused-import
-from end2end.features.test_yankpaste_bdd import init_fake_clipboard
-# pylint: enable=unused-import
+
+@pytest.fixture(autouse=True)
+def init_fake_clipboard(quteproc):
+ """Make sure the fake clipboard will be used."""
+ quteproc.send_cmd(':debug-set-fake-clipboard')
@bdd.then(bdd.parsers.parse('"{text}" should be found'))
@@ -34,6 +37,7 @@ def check_found_text(request, quteproc, text):
# https://codereview.qt-project.org/#/c/192920/
# https://codereview.qt-project.org/#/c/192921/
# https://bugreports.qt.io/browse/QTBUG-53134
+ # FIXME: Doesn't actually work, investigate why.
return
quteproc.send_cmd(':yank selection')
quteproc.wait_for(message='Setting fake clipboard: {}'.format(
diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature
index ec38116c3..e55b5839d 100644
--- a/tests/end2end/features/urlmarks.feature
+++ b/tests/end2end/features/urlmarks.feature
@@ -231,7 +231,7 @@ Feature: quickmarks and bookmarks
And the page should contain the plaintext "twentyone"
Scenario: Listing bookmarks
- When I open data/title.html in a new tab
+ When I open data/title.html#unique-url in a new tab
And I run :bookmark-add
And I open qute://bookmarks
Then the page should contain the plaintext "Test title"
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index 78fd0e48a..7ad739997 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -36,7 +36,7 @@ import pytest
from PyQt5.QtCore import pyqtSignal, QUrl
from qutebrowser.misc import ipc
-from qutebrowser.utils import log, utils, javascript, qtutils
+from qutebrowser.utils import log, utils, javascript
from helpers import utils as testutils
from end2end.fixtures import testprocess
@@ -308,6 +308,27 @@ def is_ignored_chromium_message(line):
'Could not open platform files for entry.',
'Unable to terminate process *: No such process (3)',
'Failed to read /proc/*/stat',
+
+ # Qt 5.15.1 debug build (Chromium 83)
+ # '[314297:7:0929/214605.491958:ERROR:context_provider_command_buffer.cc(145)]
+ # GpuChannelHost failed to create command buffer.'
+ 'GpuChannelHost failed to create command buffer.',
+ # [338691:4:0929/220114.488847:WARNING:ipc_message_attachment_set.cc(49)]
+ # MessageAttachmentSet destroyed with unconsumed attachments: 0/1
+ 'MessageAttachmentSet destroyed with unconsumed attachments: *',
+
+ # GitHub Actions with Qt 5.15.1
+ ('SharedImageManager::ProduceGLTexture: Trying to produce a '
+ 'representation from a non-existent mailbox. *'),
+ ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : '
+ 'DoCreateAndTexStorage2DSharedImageINTERNAL: invalid mailbox name'),
+ ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : '
+ 'DoBeginSharedImageAccessCHROMIUM: bound texture is not a shared image'),
+ ('[.DisplayCompositor]RENDER WARNING: texture bound to texture unit 0 is '
+ 'not renderable. It might be non-power-of-2 or have incompatible texture '
+ 'filtering (maybe)?'),
+ ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : '
+ 'DoEndSharedImageAccessCHROMIUM: bound texture is not a shared image'),
]
return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages)
@@ -400,8 +421,6 @@ class QuteProc(testprocess.Process):
_webengine: Whether to use QtWebEngine
basedir: The base directory for this instance.
request: The request object for the current test.
- _focus_ready: Whether the main window got focused.
- _load_ready: Whether the about:blank page got loaded.
_instance_id: A unique ID for this QuteProc instance
_run_counter: A counter to get a unique ID for each run.
@@ -418,75 +437,23 @@ class QuteProc(testprocess.Process):
super().__init__(request, parent)
self._ipc_socket = None
self.basedir = None
- self._focus_ready = False
- self._load_ready = False
self._instance_id = next(instance_counter)
self._run_counter = itertools.count()
- def _is_ready(self, what):
- """Called by _parse_line if loading/focusing is done.
-
- When both are done, emits the 'ready' signal.
- """
- if what == 'load':
- self._load_ready = True
- elif what == 'focus':
- self._focus_ready = True
- else:
- raise ValueError("Invalid value {!r} for 'what'.".format(what))
-
- is_qt_5_12 = qtutils.version_check('5.12', compiled=False)
- if ((self._load_ready and self._focus_ready) or
- (self._load_ready and is_qt_5_12)):
- self._load_ready = False
- self._focus_ready = False
- self.ready.emit()
-
def _process_line(self, log_line):
"""Check if the line matches any initial lines we're interested in."""
- start_okay_message_load = (
+ start_okay_message = (
"load status for <qutebrowser.browser.* tab_id=0 "
"url='about:blank'>: LoadStatus.success")
- start_okay_messages_focus = [
- ## QtWebKit
- "Focus object changed: "
- "<qutebrowser.browser.* tab_id=0 url='about:blank'>",
- # when calling QApplication::sync
- "Focus object changed: "
- "<qutebrowser.browser.webkit.webview.WebView tab_id=0 url=''>",
-
- ## QtWebEngine
- "Focus object changed: "
- "<PyQt5.QtWidgets.QOpenGLWidget object at *>",
- # with Qt >= 5.8
- "Focus object changed: "
- "<PyQt5.QtGui.QWindow object at *>",
- # when calling QApplication::sync
- "Focus object changed: "
- "<PyQt5.QtWidgets.QWidget object at *>",
- # Qt >= 5.11
- "Focus object changed: "
- "<qutebrowser.browser.webengine.webview.WebEngineView object "
- "at *>",
- # Qt >= 5.11 with workarounds
- "Focus object changed: "
- "<PyQt5.QtQuickWidgets.QQuickWidget object at *>",
- ]
if (log_line.category == 'ipc' and
log_line.message.startswith("Listening as ")):
self._ipc_socket = log_line.message.split(' ', maxsplit=2)[2]
elif (log_line.category == 'webview' and
- testutils.pattern_match(pattern=start_okay_message_load,
+ testutils.pattern_match(pattern=start_okay_message,
value=log_line.message)):
- if not self._load_ready:
- log_line.waited_for = True
- self._is_ready('load')
- elif (log_line.category == 'misc' and any(
- testutils.pattern_match(pattern=pattern,
- value=log_line.message)
- for pattern in start_okay_messages_focus)):
- self._is_ready('focus')
+ log_line.waited_for = True
+ self.ready.emit()
elif (log_line.category == 'init' and
log_line.module == 'standarddir' and
log_line.function == 'init' and
@@ -717,11 +684,7 @@ class QuteProc(testprocess.Process):
target_arg)
self._wait_for_ipc()
- def start(self, *args, wait_focus=True,
- **kwargs): # pylint: disable=arguments-differ
- if not wait_focus:
- self._focus_ready = True
-
+ def start(self, *args, **kwargs): # pylint: disable=arguments-differ
try:
super().start(*args, **kwargs)
except testprocess.ProcessExited:
@@ -749,6 +712,7 @@ class QuteProc(testprocess.Process):
Return:
The parsed log line with "command called: ..." or None.
"""
+ __tracebackhide__ = lambda e: e.errisinstance(testprocess.WaitForTimeout)
summary = command
if count is not None:
summary += ' (count {})'.format(count)
@@ -855,24 +819,16 @@ class QuteProc(testprocess.Process):
raise ValueError("Invalid URL {}: {}".format(url,
qurl.errorString()))
- if (qurl == QUrl('about:blank') and
- not qtutils.version_check('5.10', compiled=False)):
- # For some reason, we don't get a LoadStatus.success for
- # about:blank sometimes.
- # However, if we do this for Qt 5.10, we get general testsuite
- # instability as site loads get reported with about:blank...
- pattern = "Changing title for idx * to 'about:blank'"
- else:
- # We really need the same representation that the webview uses in
- # its __repr__
- url = utils.elide(qurl.toDisplayString(QUrl.EncodeUnicode), 100)
- assert url
-
- pattern = re.compile(
- r"(load status for <qutebrowser\.browser\..* "
- r"tab_id=\d+ url='{url}/?'>: LoadStatus\.{load_status}|fetch: "
- r"PyQt5\.QtCore\.QUrl\('{url}'\) -> .*)".format(
- load_status=re.escape(load_status), url=re.escape(url)))
+ # We really need the same representation that the webview uses in
+ # its __repr__
+ url = utils.elide(qurl.toDisplayString(QUrl.EncodeUnicode), 100)
+ assert url
+
+ pattern = re.compile(
+ r"(load status for <qutebrowser\.browser\..* "
+ r"tab_id=\d+ url='{url}/?'>: LoadStatus\.{load_status}|fetch: "
+ r"PyQt5\.QtCore\.QUrl\('{url}'\) -> .*)".format(
+ load_status=re.escape(load_status), url=re.escape(url)))
try:
self.wait_for(message=pattern, timeout=timeout, after=after)
diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py
index 814d16806..51352c539 100644
--- a/tests/end2end/fixtures/testprocess.py
+++ b/tests/end2end/fixtures/testprocess.py
@@ -22,6 +22,7 @@
import re
import os
import time
+import warnings
import attr
import pytest
@@ -100,7 +101,7 @@ def pytest_runtest_makereport(item, call):
return
quteproc_log = getattr(item, '_quteproc_log', None)
- server_log = getattr(item, '_server_log', None)
+ server_logs = getattr(item, '_server_logs', [])
if not hasattr(report.longrepr, 'addsection'):
# In some conditions (on macOS and Windows it seems), report.longrepr
@@ -113,11 +114,11 @@ def pytest_runtest_makereport(item, call):
verbose = item.config.getoption('--verbose')
if quteproc_log is not None:
- report.longrepr.addsection("qutebrowser output",
- _render_log(quteproc_log, verbose=verbose))
- if server_log is not None:
- report.longrepr.addsection("server output",
- _render_log(server_log, verbose=verbose))
+ report.longrepr.addsection(
+ "qutebrowser output", _render_log(quteproc_log, verbose=verbose))
+ for name, content in server_logs:
+ report.longrepr.addsection(
+ f"{name} output", _render_log(content, verbose=verbose))
class Process(QObject):
@@ -316,8 +317,11 @@ class Process(QObject):
else:
self.proc.terminate()
- ok = self.proc.waitForFinished()
+ ok = self.proc.waitForFinished(5000)
if not ok:
+ cmdline = ' '.join([self.proc.program()] + self.proc.arguments())
+ warnings.warn(f"Test process {cmdline} with PID {self.proc.processId()} "
+ "failed to terminate!")
self.proc.kill()
self.proc.waitForFinished()
diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py
index 839355664..d40739724 100644
--- a/tests/end2end/fixtures/webserver.py
+++ b/tests/end2end/fixtures/webserver.py
@@ -23,6 +23,7 @@ import re
import sys
import json
import os.path
+import socket
from http import HTTPStatus
import attr
@@ -31,8 +32,6 @@ from PyQt5.QtCore import pyqtSignal, QUrl
from end2end.fixtures import testprocess
-from qutebrowser.utils import utils
-
class Request(testprocess.Line):
@@ -140,9 +139,17 @@ class WebserverProcess(testprocess.Process):
def __init__(self, request, script, parent=None):
super().__init__(request, parent)
self._script = script
- self.port = utils.random_port()
+ self.port = self._random_port()
self.new_data.connect(self.new_request)
+ 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
+
def get_requests(self):
"""Get the requests to the server during this test."""
requests = self._get_data()
@@ -185,21 +192,41 @@ def server(qapp, request):
@pytest.fixture(autouse=True)
def server_per_test(server, request):
"""Fixture to clean server request list after each test."""
- request.node._server_log = server.captured_log
+ if not hasattr(request.node, '_server_logs'):
+ request.node._server_logs = []
+ request.node._server_logs.append(('server', server.captured_log))
+
server.before_test()
yield
server.after_test()
@pytest.fixture
+def server2(qapp, request):
+ """Fixture for a second server object for cross-origin tests."""
+ server = WebserverProcess(request, 'webserver_sub')
+
+ if not hasattr(request.node, '_server_logs'):
+ request.node._server_logs = []
+ request.node._server_logs.append(('secondary server', server.captured_log))
+
+ server.start()
+ yield server
+ server.terminate()
+
+
+@pytest.fixture
def ssl_server(request, qapp):
"""Fixture for a webserver with a self-signed SSL certificate.
- This needs to be explicitly used in a test, and overwrites the server log
- used in that test.
+ This needs to be explicitly used in a test.
"""
server = WebserverProcess(request, 'webserver_sub_ssl')
- request.node._server_log = server.captured_log
+
+ if not hasattr(request.node, '_server_logs'):
+ request.node._server_logs = []
+ request.node._server_logs.append(('SSL server', server.captured_log))
+
server.start()
yield server
server.after_test()
diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py
index 9902ab125..e38d64bb8 100644
--- a/tests/end2end/fixtures/webserver_sub.py
+++ b/tests/end2end/fixtures/webserver_sub.py
@@ -29,9 +29,8 @@ parameters or headers with the same name properly.
import sys
import json
import time
-import signal
-import os
import threading
+import pathlib
from http import HTTPStatus
import cheroot.wsgi
@@ -41,6 +40,9 @@ app = flask.Flask(__name__)
_redirect_later_event = None
+END2END_DIR = pathlib.Path(__file__).resolve().parents[1]
+
+
@app.route('/')
def root():
"""Show simple text."""
@@ -55,15 +57,8 @@ def send_data(path):
If a directory is requested, its index.html is sent.
"""
- if hasattr(sys, 'frozen'):
- basedir = os.path.realpath(os.path.dirname(sys.executable))
- data_dir = os.path.join(basedir, 'end2end', 'data')
- else:
- basedir = os.path.join(os.path.realpath(os.path.dirname(__file__)),
- '..')
- data_dir = os.path.join(basedir, 'data')
- print(basedir)
- if os.path.isdir(os.path.join(data_dir, path)):
+ data_dir = END2END_DIR / 'data'
+ if (data_dir / path).is_dir():
path += '/index.html'
return flask.send_from_directory(data_dir, path)
@@ -249,6 +244,12 @@ def view_headers():
return flask.jsonify(headers=dict(flask.request.headers))
+@app.route('/headers-link/<int:port>')
+def headers_link(port):
+ """Get a (possibly cross-origin) link to /headers."""
+ return flask.render_template('headers-link.html', port=port)
+
+
@app.route('/response-headers')
def response_headers():
"""Return a set of response headers from the query string."""
@@ -274,11 +275,9 @@ def view_user_agent():
@app.route('/favicon.ico')
def favicon():
- basedir = os.path.join(os.path.realpath(os.path.dirname(__file__)),
- '..', '..', '..')
- return flask.send_from_directory(os.path.join(basedir, 'icons'),
- 'qutebrowser.ico',
- mimetype='image/vnd.microsoft.icon')
+ icon_dir = END2END_DIR.parents[1] / 'icons'
+ return flask.send_from_directory(
+ icon_dir, 'qutebrowser.ico', mimetype='image/vnd.microsoft.icon')
@app.after_request
@@ -322,18 +321,12 @@ class WSGIServer(cheroot.wsgi.Server):
def main():
- if hasattr(sys, 'frozen'):
- basedir = os.path.realpath(os.path.dirname(sys.executable))
- app.template_folder = os.path.join(basedir, 'end2end', 'templates')
+ app.template_folder = END2END_DIR / 'templates'
+ assert app.template_folder.is_dir(), app.template_folder
+
port = int(sys.argv[1])
server = WSGIServer(('127.0.0.1', port), app)
-
- signal.signal(signal.SIGTERM, lambda *args: server.stop())
-
- try:
- server.start()
- except KeyboardInterrupt:
- server.stop()
+ server.start()
if __name__ == '__main__':
diff --git a/tests/end2end/templates/headers-link.html b/tests/end2end/templates/headers-link.html
new file mode 100644
index 000000000..fece530b1
--- /dev/null
+++ b/tests/end2end/templates/headers-link.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Link to header page</title>
+ </head>
+ <body>
+ <a href="http://localhost:{{ port }}/headers" id="link">headers</a>
+ </body>
+</html>
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
index cd9aefe16..74805cec2 100644
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -20,16 +20,15 @@
"""Test starting qutebrowser with special arguments/environments."""
import subprocess
-import socket
import sys
import logging
import re
+import json
import pytest
from PyQt5.QtCore import QProcess
from helpers import utils
-from qutebrowser.utils import qtutils
ascii_locale = pytest.mark.skipif(sys.hexversion >= 0x03070000,
@@ -71,7 +70,6 @@ def temp_basedir_env(tmpdir, short_tmpdir):
'[general]',
'quickstart-done = 1',
'backend-warning-shown = 1',
- 'old-qt-warning-shown = 1',
'webkit-warning-shown = 1',
]
@@ -162,7 +160,7 @@ def test_open_command_line_with_ascii_locale(request, server, tmpdir,
# all be called. No exception thrown means test success.
args = (['--temp-basedir'] + _base_args(request.config) +
['/home/user/föö.html'])
- quteproc_new.start(args, env={'LC_ALL': 'C'}, wait_focus=False)
+ quteproc_new.start(args, env={'LC_ALL': 'C'})
if not request.config.webengine:
line = quteproc_new.wait_for(message="Error while loading *: Error "
@@ -252,10 +250,7 @@ def test_version(request):
print(stderr)
assert ok
-
- if qtutils.version_check('5.9'):
- # Segfaults on exit with Qt 5.7
- assert proc.exitStatus() == QProcess.NormalExit
+ assert proc.exitStatus() == QProcess.NormalExit
match = re.search(r'^qutebrowser\s+v\d+(\.\d+)', stdout, re.MULTILINE)
assert match is not None
@@ -275,23 +270,6 @@ def test_qt_arg(request, quteproc_new, tmpdir):
quteproc_new.wait_for_quit()
-@utils.skip_qt511
-def test_webengine_inspector(request, quteproc_new):
- if not request.config.webengine:
- pytest.skip()
- args = (['--temp-basedir', '--enable-webengine-inspector'] +
- _base_args(request.config))
- quteproc_new.start(args)
- line = quteproc_new.wait_for(
- message='Remote debugging server started successfully. Try pointing a '
- 'Chromium-based browser to http://127.0.0.1:*')
- port = int(line.message.split(':')[-1])
-
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.connect(('127.0.0.1', port))
- s.close()
-
-
@pytest.mark.linux
def test_webengine_download_suffix(request, quteproc_new, tmpdir):
"""Make sure QtWebEngine does not add a suffix to downloads."""
@@ -333,16 +311,17 @@ def test_command_on_start(request, quteproc_new):
quteproc_new.wait_for_quit()
-def test_launching_with_python2():
+@pytest.mark.parametrize('python', ['python2', 'python3.5'])
+def test_launching_with_old_python(python):
try:
proc = subprocess.run(
- ['python2', '-m', 'qutebrowser', '--no-err-windows'],
+ [python, '-m', 'qutebrowser', '--no-err-windows'],
stderr=subprocess.PIPE,
check=False)
except FileNotFoundError:
- pytest.skip("python2 not found")
+ pytest.skip(f"{python} not found")
assert proc.returncode == 1
- error = "At least Python 3.5.2 is required to run qutebrowser"
+ error = "At least Python 3.6 is required to run qutebrowser"
assert proc.stderr.decode('ascii').startswith(error)
@@ -405,3 +384,50 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
quteproc_new.send_cmd(':quit')
quteproc_new.wait_for_quit()
+
+
+@pytest.mark.parametrize('value, expected', [
+ ('always', 'http://localhost:(port2)/headers-link/(port)'),
+ ('never', None),
+ ('same-domain', 'http://localhost:(port2)/'), # None with QtWebKit
+])
+def test_referrer(quteproc_new, server, server2, request, value, expected):
+ """Check referrer settings."""
+ args = _base_args(request.config) + [
+ '--temp-basedir',
+ '-s', 'content.headers.referer', value,
+ ]
+ quteproc_new.start(args)
+
+ quteproc_new.open_path(f'headers-link/{server.port}', port=server2.port)
+ quteproc_new.send_cmd(':click-element id link')
+ quteproc_new.wait_for_load_finished('headers')
+
+ content = quteproc_new.get_content()
+ data = json.loads(content)
+ print(data)
+ headers = data['headers']
+
+ if not request.config.webengine and value == 'same-domain':
+ # With QtWebKit and same-domain, we don't send a referer at all.
+ expected = None
+
+ if expected is not None:
+ for key, val in [('(port)', server.port), ('(port2)', server2.port)]:
+ expected = expected.replace(key, str(val))
+
+ assert headers.get('Referer') == expected
+
+
+@pytest.mark.qtwebkit_skip
+@utils.qt514
+def test_preferred_colorscheme(request, quteproc_new):
+ """Make sure the the preferred colorscheme is set."""
+ args = _base_args(request.config) + [
+ '--temp-basedir',
+ '-s', 'colors.webpage.prefers_color_scheme_dark', 'true',
+ ]
+ quteproc_new.start(args)
+
+ quteproc_new.send_cmd(':jseval matchMedia("(prefers-color-scheme: dark)").matches')
+ quteproc_new.wait_for(message='True')
diff --git a/tests/end2end/test_mkvenv.py b/tests/end2end/test_mkvenv.py
new file mode 100644
index 000000000..430be0279
--- /dev/null
+++ b/tests/end2end/test_mkvenv.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2020 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 <http://www.gnu.org/licenses/>.
+
+
+from scripts import mkvenv
+
+
+def test_smoke(tmp_path):
+ """Simple smoke test of mkvenv.py."""
+ args = mkvenv.parse_args(['--venv-dir', str(tmp_path / 'venv'), '--skip-docs'])
+ mkvenv.run(args)
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index 015238d1b..6f80099bb 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -45,7 +45,7 @@ import helpers.stubs as stubsmod
from qutebrowser.config import (config, configdata, configtypes, configexc,
configfiles, configcache, stylesheet)
from qutebrowser.api import config as configapi
-from qutebrowser.utils import objreg, standarddir, utils, usertypes, qtutils
+from qutebrowser.utils import objreg, standarddir, utils, usertypes
from qutebrowser.browser import greasemonkey, history, qutescheme
from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.misc import savemanager, sql, objects, sessions
@@ -97,6 +97,10 @@ class WinRegistryHelper:
def windowTitle(self):
return 'window title - qutebrowser'
+ @property
+ def tabbed_browser(self):
+ return self.registry['tabbed-browser']
+
def __init__(self):
self._ids = []
@@ -162,7 +166,7 @@ def fake_web_tab(stubs, tab_registry, mode_manager, qapp):
@pytest.fixture
-def greasemonkey_manager(monkeypatch, data_tmpdir):
+def greasemonkey_manager(monkeypatch, data_tmpdir, config_tmpdir):
gm_manager = greasemonkey.GreasemonkeyManager()
monkeypatch.setattr(greasemonkey, 'gm_manager', gm_manager)
@@ -254,11 +258,7 @@ def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data,
# If we wait for the GC to clean things up, there's a segfault inside
# QtWebEngine sometimes (e.g. if we only run
# tests/unit/browser/test_caret.py).
- # However, with Qt < 5.12, doing this here will lead to an immediate
- # segfault...
- monkeypatch.undo() # version_check could be patched
- if qtutils.version_check('5.12'):
- sip.delete(tab._widget)
+ sip.delete(tab._widget)
@pytest.fixture(params=['webkit', 'webengine'])
@@ -705,3 +705,16 @@ def state_config(data_tmpdir, monkeypatch):
state = configfiles.StateConfig()
monkeypatch.setattr(configfiles, 'state', state)
return state
+
+
+@pytest.fixture
+def unwritable_tmp_path(tmp_path):
+ tmp_path.chmod(0)
+ if os.access(str(tmp_path), os.W_OK):
+ # Docker container or similar
+ pytest.skip("Directory was still writable")
+
+ yield tmp_path
+
+ # Make sure pytest can clean up the tmp_path
+ tmp_path.chmod(0o755)
diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py
index 4c1107029..03320a98f 100644
--- a/tests/helpers/messagemock.py
+++ b/tests/helpers/messagemock.py
@@ -24,7 +24,7 @@ import logging
import attr
import pytest
-from qutebrowser.utils import usertypes, message, objreg
+from qutebrowser.utils import usertypes, message
@attr.s
@@ -90,12 +90,3 @@ def message_mock():
mmock.patch()
yield mmock
mmock.unpatch()
-
-
-@pytest.fixture
-def message_bridge(win_registry):
- """Fixture to get a MessageBridge."""
- bridge = message.MessageBridge()
- objreg.register('message-bridge', bridge, scope='window', window=0)
- yield bridge
- objreg.delete('message-bridge', scope='window', window=0)
diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py
index f9223c3ca..725df8fe8 100644
--- a/tests/helpers/stubs.py
+++ b/tests/helpers/stubs.py
@@ -670,8 +670,8 @@ class FakeWebEngineProfile:
class FakeCookieStore:
- def __init__(self, has_cookie_filter):
+ def __init__(self):
self.cookie_filter = None
- if has_cookie_filter:
- self.setCookieFilter = (
- lambda func: setattr(self, 'cookie_filter', func)) # noqa
+
+ def setCookieFilter(self, func):
+ self.cookie_filter = func
diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py
index 46787391d..2c275bf15 100644
--- a/tests/helpers/utils.py
+++ b/tests/helpers/utils.py
@@ -41,16 +41,8 @@ from qutebrowser.utils import qtutils, log
ON_CI = 'CI' in os.environ
-qt58 = pytest.mark.skipif(
- qtutils.version_check('5.9'), reason="Needs Qt 5.8 or earlier")
-qt59 = pytest.mark.skipif(
- not qtutils.version_check('5.9'), reason="Needs Qt 5.9 or newer")
-qt510 = pytest.mark.skipif(
- not qtutils.version_check('5.10'), reason="Needs Qt 5.10 or newer")
qt514 = pytest.mark.skipif(
not qtutils.version_check('5.14'), reason="Needs Qt 5.14 or newer")
-skip_qt511 = pytest.mark.skipif(
- qtutils.version_check('5.11'), reason="Needs Qt 5.10 or earlier")
class PartialCompareOutcome:
@@ -234,7 +226,7 @@ def change_cwd(path):
@contextlib.contextmanager
def ignore_bs4_warning():
"""WORKAROUND for https://bugs.launchpad.net/beautifulsoup/+bug/1847592."""
- with log.ignore_py_warnings(
+ with log.py_warning_filter(
category=DeprecationWarning,
message="Using or importing the ABCs from 'collections' instead "
"of from 'collections.abc' is deprecated", module='bs4.element'):
@@ -257,15 +249,6 @@ def seccomp_args(qt_flag):
"""
affected_versions = set()
for base, patch_range in [
- ## seccomp-bpf failure in syscall 0281
- ## https://github.com/qutebrowser/qutebrowser/issues/3163
- # 5.7.1
- ('5.7', [1]),
-
- ## seccomp-bpf failure in syscall 0281 (clock_nanosleep)
- ## https://bugreports.qt.io/browse/QTBUG-81313
- # 5.11.0 to 5.11.3 (inclusive)
- ('5.11', range(0, 4)),
# 5.12.0 to 5.12.7 (inclusive)
('5.12', range(0, 8)),
# 5.13.0 to 5.13.2 (inclusive)
diff --git a/tests/manual/hints/hide_unmatched_rapid_hints.html b/tests/manual/hints/hide_unmatched_rapid_hints.html
index 1630a790e..98affa254 100644
--- a/tests/manual/hints/hide_unmatched_rapid_hints.html
+++ b/tests/manual/hints/hide_unmatched_rapid_hints.html
@@ -7,7 +7,7 @@
<body>
<p>When <code>hints.hide_unmatched_rapid_hints</code> is set to true (default), rapid hints behave like normal hints, i.e. unmatched hints will be hidden as you type. Setting the option to false will disable hiding in rapid mode, which is sometimes useful (see <a href="https://github.com/qutebrowser/qutebrowser/issues/1799">#1799</a>).</p>
<p>Note that when hinting in number mode, the <code>hints.hide_unmatched_rapid_hints</code> option affects typing the hint string (number), but not the filter (letters).</p>
- <p>Here is couple of invalid links to test the behaviour:</p>
+ <p>Here is couple of invalid links to test the behavior:</p>
<p><a href="#foo">one</a></p>
<p><a href="#foo">two</a></p>
<p><a href="#foo">three</a></p>
diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py
index 58643640c..811ba2659 100644
--- a/tests/unit/api/test_cmdutils.py
+++ b/tests/unit/api/test_cmdutils.py
@@ -24,8 +24,8 @@
import sys
import logging
import types
-import typing
import enum
+from typing import Union
import pytest
@@ -290,24 +290,28 @@ class TestRegister:
else:
assert pos_args == [('arg', 'arg')]
- Enum = enum.Enum('Test', ['x', 'y'])
+ class Enum(enum.Enum):
+
+ # pylint: disable=invalid-name
+ x = enum.auto()
+ y = enum.auto()
@pytest.mark.parametrize('typ, inp, choices, expected', [
(int, '42', None, 42),
(int, 'x', None, cmdexc.ArgumentTypeError),
(str, 'foo', None, 'foo'),
- (typing.Union[str, int], 'foo', None, 'foo'),
- (typing.Union[str, int], '42', None, 42),
+ (Union[str, int], 'foo', None, 'foo'),
+ (Union[str, int], '42', None, 42),
# Choices
(str, 'foo', ['foo'], 'foo'),
(str, 'bar', ['foo'], cmdexc.ArgumentTypeError),
# Choices with Union: only checked when it's a str
- (typing.Union[str, int], 'foo', ['foo'], 'foo'),
- (typing.Union[str, int], 'bar', ['foo'], cmdexc.ArgumentTypeError),
- (typing.Union[str, int], '42', ['foo'], 42),
+ (Union[str, int], 'foo', ['foo'], 'foo'),
+ (Union[str, int], 'bar', ['foo'], cmdexc.ArgumentTypeError),
+ (Union[str, int], '42', ['foo'], 42),
(Enum, 'x', None, Enum.x),
(Enum, 'z', None, cmdexc.ArgumentTypeError),
diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py
index 75d9fee09..bfa718315 100644
--- a/tests/unit/browser/test_caret.py
+++ b/tests/unit/browser/test_caret.py
@@ -324,9 +324,6 @@ def test_drop_selection(caret, selection):
class TestSearch:
- # https://bugreports.qt.io/browse/QTBUG-60673
-
- @pytest.mark.qtbug60673
@pytest.mark.no_xvfb
def test_yanking_a_searched_line(self, caret, selection, mode_manager, web_tab, qtbot):
mode_manager.leave(usertypes.KeyMode.caret)
@@ -339,7 +336,6 @@ class TestSearch:
caret.move_to_end_of_line()
selection.check('five six')
- @pytest.mark.qtbug60673
@pytest.mark.no_xvfb
def test_yanking_a_searched_line_with_multiple_matches(self, caret, selection, mode_manager, web_tab, qtbot):
mode_manager.leave(usertypes.KeyMode.caret)
diff --git a/tests/unit/browser/test_hints.py b/tests/unit/browser/test_hints.py
index 56e5b980c..3b09ed0fd 100644
--- a/tests/unit/browser/test_hints.py
+++ b/tests/unit/browser/test_hints.py
@@ -30,10 +30,8 @@ import qutebrowser.browser.hints
@pytest.fixture(autouse=True)
-def setup(benchmark, win_registry, mode_manager):
- yield
- # WORKAROUND for https://github.com/ionelmc/pytest-benchmark/issues/125
- benchmark._mode = 'WORKAROUND' # pylint: disable=protected-access
+def setup(win_registry, mode_manager):
+ pass
@pytest.fixture
@@ -46,8 +44,7 @@ def tabbed_browser(tabbed_browser_stubs, web_tab):
return tb
-def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
- mode_manager):
+def test_show_benchmark(benchmark, tabbed_browser, qtbot, mode_manager):
"""Benchmark showing/drawing of hint labels."""
tab = tabbed_browser.widget.tabs[0]
@@ -66,8 +63,8 @@ def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
benchmark(bench)
-def test_match_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
- mode_manager, qapp, config_stub):
+def test_match_benchmark(benchmark, tabbed_browser, qtbot, mode_manager, qapp,
+ config_stub):
"""Benchmark matching of hint labels."""
tab = tabbed_browser.widget.tabs[0]
diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py
index c70b858f5..c1990de0d 100644
--- a/tests/unit/browser/test_history.py
+++ b/tests/unit/browser/test_history.py
@@ -464,7 +464,8 @@ class TestCompletionMetaInfo:
def test_contains_keyerror(self, metainfo):
with pytest.raises(KeyError):
- 'does_not_exist' in metainfo # pylint: disable=pointless-statement
+ # pylint: disable=pointless-statement
+ 'does_not_exist' in metainfo # noqa: B015
def test_getitem_keyerror(self, metainfo):
with pytest.raises(KeyError):
diff --git a/tests/unit/browser/test_navigate.py b/tests/unit/browser/test_navigate.py
index 5fe0acbf6..5a93a517c 100644
--- a/tests/unit/browser/test_navigate.py
+++ b/tests/unit/browser/test_navigate.py
@@ -187,6 +187,8 @@ class TestUp:
('/one/two/three', 1, '/one/two'),
('/one/two/three?foo=bar', 1, '/one/two'),
('/one/two/three', 2, '/one'),
+ ('/one/two%2Fthree', 1, '/one'),
+ ('/one/two%2Fthree/four', 1, '/one/two%2Fthree'),
])
def test_up(self, url_suffix, count, expected_suffix):
url_base = 'https://example.com'
diff --git a/tests/unit/browser/test_pdfjs.py b/tests/unit/browser/test_pdfjs.py
index d05ff1fc0..465244819 100644
--- a/tests/unit/browser/test_pdfjs.py
+++ b/tests/unit/browser/test_pdfjs.py
@@ -24,7 +24,7 @@ import pytest
from PyQt5.QtCore import QUrl
from qutebrowser.browser import pdfjs
-from qutebrowser.utils import usertypes, utils, urlmatch
+from qutebrowser.utils import urlmatch
pytestmark = [pytest.mark.usefixtures('data_tmpdir')]
@@ -52,15 +52,6 @@ def test_generate_pdfjs_page(available, snippet, monkeypatch):
assert snippet in content
-def test_broken_installation(data_tmpdir, monkeypatch):
- """Make sure we don't crash with a broken local installation."""
- monkeypatch.setattr(pdfjs, '_SYSTEM_PATHS', [])
- (data_tmpdir / 'pdfjs' / 'pdf.js').ensure() # But no viewer.html
-
- content = pdfjs.generate_pdfjs_page('example.pdf', QUrl())
- assert '<h1>No pdf.js installation found</h1>' in content
-
-
# Note that we got double protection, once because we use QUrl.FullyEncoded and
# because we use qutebrowser.utils.javascript.to_js. Characters like " are
# already replaced by QUrl.
@@ -78,36 +69,6 @@ def test_generate_pdfjs_script(filename, expected):
assert 'PDFView' in actual
-@pytest.mark.parametrize('qt, backend, expected', [
- ('new', usertypes.Backend.QtWebEngine, False),
- ('new', usertypes.Backend.QtWebKit, False),
- ('old', usertypes.Backend.QtWebEngine, True),
- ('old', usertypes.Backend.QtWebKit, False),
- ('5.7', usertypes.Backend.QtWebEngine, False),
- ('5.7', usertypes.Backend.QtWebKit, False),
-])
-def test_generate_pdfjs_script_disable_object_url(monkeypatch,
- qt, backend, expected):
- if qt == 'new':
- monkeypatch.setattr(pdfjs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- version != '5.7.1')
- elif qt == 'old':
- monkeypatch.setattr(pdfjs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True: False)
- elif qt == '5.7':
- monkeypatch.setattr(pdfjs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- version == '5.7.1')
- else:
- raise utils.Unreachable
-
- monkeypatch.setattr(pdfjs.objects, 'backend', backend)
-
- script = pdfjs._generate_pdfjs_script('testfile')
- assert ('PDFJS.disableCreateObjectURL' in script) == expected
-
-
class TestResources:
@pytest.fixture
@@ -166,6 +127,19 @@ class TestResources:
expected = 'OSError while reading PDF.js file: Message'
assert caplog.messages == [expected]
+ def test_broken_installation(self, data_tmpdir, tmpdir, monkeypatch,
+ read_file_mock):
+ """Make sure we don't crash with a broken local installation."""
+ monkeypatch.setattr(pdfjs, '_SYSTEM_PATHS', [])
+ monkeypatch.setattr(pdfjs.os.path, 'expanduser',
+ lambda _in: tmpdir / 'fallback')
+ read_file_mock.side_effect = FileNotFoundError
+
+ (data_tmpdir / 'pdfjs' / 'pdf.js').ensure() # But no viewer.html
+
+ content = pdfjs.generate_pdfjs_page('example.pdf', QUrl())
+ assert '<h1>No pdf.js installation found</h1>' in content
+
@pytest.mark.parametrize('path, expected', [
('web/viewer.js', 'viewer.js'),
diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py
index d4a87c4f9..0d813df02 100644
--- a/tests/unit/browser/test_qutescheme.py
+++ b/tests/unit/browser/test_qutescheme.py
@@ -28,6 +28,7 @@ from PyQt5.QtCore import QUrl, QUrlQuery
import pytest
from qutebrowser.browser import qutescheme, pdfjs, downloads
+from qutebrowser.utils import utils
class TestJavascriptHandler:
@@ -51,7 +52,7 @@ class TestJavascriptHandler:
return content
raise OSError("File not found {}!".format(path))
- monkeypatch.setattr('qutebrowser.utils.utils.read_file', _read_file)
+ monkeypatch.setattr(utils, 'read_file', _read_file)
@pytest.mark.parametrize("filename, content", js_files)
def test_qutejavascript(self, filename, content):
diff --git a/tests/unit/browser/webengine/test_darkmode.py b/tests/unit/browser/webengine/test_darkmode.py
new file mode 100644
index 000000000..cd84526c3
--- /dev/null
+++ b/tests/unit/browser/webengine/test_darkmode.py
@@ -0,0 +1,263 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+# Copyright 2020 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 <http://www.gnu.org/licenses/>.
+
+import logging
+
+import pytest
+
+from qutebrowser.config import configdata
+from qutebrowser.utils import usertypes, version
+from qutebrowser.browser.webengine import darkmode
+from qutebrowser.misc import objects
+from helpers import utils
+
+
+@pytest.fixture(autouse=True)
+def patch_backend(monkeypatch):
+ monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
+
+
+@pytest.mark.parametrize('qversion, enabled, expected', [
+ # Disabled or nothing set
+ ("5.14", False, []),
+ ("5.15.0", False, []),
+ ("5.15.1", False, []),
+ ("5.15.2", False, []),
+
+ # Enabled in configuration
+ ("5.14", True, []),
+ ("5.15.0", True, []),
+ ("5.15.1", True, []),
+ ("5.15.2", True, [("preferredColorScheme", "1")]),
+])
+@utils.qt514
+def test_colorscheme(config_stub, monkeypatch, qversion, enabled, expected):
+ monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion)
+ config_stub.val.colors.webpage.prefers_color_scheme_dark = enabled
+ assert list(darkmode.settings()) == expected
+
+
+@pytest.mark.parametrize('settings, expected', [
+ # Disabled
+ ({}, []),
+
+ # Enabled without customization
+ ({'enabled': True}, [('forceDarkModeEnabled', 'true')]),
+
+ # Algorithm
+ (
+ {'enabled': True, 'algorithm': 'brightness-rgb'},
+ [
+ ('forceDarkModeEnabled', 'true'),
+ ('forceDarkModeInversionAlgorithm', '2')
+ ],
+ ),
+])
+def test_basics(config_stub, monkeypatch, settings, expected):
+ for k, v in settings.items():
+ config_stub.set_obj('colors.webpage.darkmode.' + k, v)
+ monkeypatch.setattr(darkmode, '_variant',
+ lambda: darkmode.Variant.qt_515_2)
+
+ if expected:
+ expected.append(('forceDarkModeImagePolicy', '2'))
+
+ assert list(darkmode.settings()) == expected
+
+
+QT_514_SETTINGS = [
+ ('darkMode', '2'),
+ ('darkModeImagePolicy', '2'),
+ ('darkModeGrayscale', 'true'),
+]
+
+
+QT_515_0_SETTINGS = [
+ ('darkModeEnabled', 'true'),
+ ('darkModeInversionAlgorithm', '2'),
+ ('darkModeGrayscale', 'true'),
+]
+
+
+QT_515_1_SETTINGS = [
+ ('darkModeEnabled', 'true'),
+ ('darkModeInversionAlgorithm', '2'),
+ ('darkModeImagePolicy', '2'),
+ ('darkModeGrayscale', 'true'),
+]
+
+
+QT_515_2_SETTINGS = [
+ ('forceDarkModeEnabled', 'true'),
+ ('forceDarkModeInversionAlgorithm', '2'),
+ ('forceDarkModeImagePolicy', '2'),
+ ('forceDarkModeGrayscale', 'true'),
+]
+
+
+@pytest.mark.parametrize('qversion, expected', [
+ ('5.14.0', QT_514_SETTINGS),
+ ('5.14.1', QT_514_SETTINGS),
+ ('5.14.2', QT_514_SETTINGS),
+
+ ('5.15.0', QT_515_0_SETTINGS),
+ ('5.15.1', QT_515_1_SETTINGS),
+
+ ('5.15.2', QT_515_2_SETTINGS),
+])
+def test_qt_version_differences(config_stub, monkeypatch, qversion, expected):
+ monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion)
+
+ major, minor, patch = [int(part) for part in qversion.split('.')]
+ hexversion = major << 16 | minor << 8 | patch
+ if major > 5 or minor >= 13:
+ # Added in Qt 5.13
+ monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', hexversion)
+
+ settings = {
+ 'enabled': True,
+ 'algorithm': 'brightness-rgb',
+ 'grayscale.all': True,
+ }
+ for k, v in settings.items():
+ config_stub.set_obj('colors.webpage.darkmode.' + k, v)
+
+ assert list(darkmode.settings()) == expected
+
+
+@utils.qt514
+@pytest.mark.parametrize('setting, value, exp_key, exp_val', [
+ ('contrast', -0.5,
+ 'Contrast', '-0.5'),
+ ('policy.page', 'smart',
+ 'PagePolicy', '1'),
+ ('policy.images', 'smart',
+ 'ImagePolicy', '2'),
+ ('threshold.text', 100,
+ 'TextBrightnessThreshold', '100'),
+ ('threshold.background', 100,
+ 'BackgroundBrightnessThreshold', '100'),
+ ('grayscale.all', True,
+ 'Grayscale', 'true'),
+ ('grayscale.images', 0.5,
+ 'ImageGrayscale', '0.5'),
+])
+def test_customization(config_stub, monkeypatch, setting, value, exp_key, exp_val):
+ config_stub.val.colors.webpage.darkmode.enabled = True
+ config_stub.set_obj('colors.webpage.darkmode.' + setting, value)
+ monkeypatch.setattr(darkmode, '_variant', lambda: darkmode.Variant.qt_515_2)
+
+ expected = []
+ expected.append(('forceDarkModeEnabled', 'true'))
+ if exp_key != 'ImagePolicy':
+ expected.append(('forceDarkModeImagePolicy', '2'))
+ expected.append(('forceDarkMode' + exp_key, exp_val))
+
+ assert list(darkmode.settings()) == expected
+
+
+@pytest.mark.parametrize('qversion, webengine_version, expected', [
+ # Without PYQT_WEBENGINE_VERSION
+ ('5.12.9', None, darkmode.Variant.qt_511_to_513),
+
+ # With PYQT_WEBENGINE_VERSION
+ (None, 0x050d00, darkmode.Variant.qt_511_to_513),
+ (None, 0x050e00, darkmode.Variant.qt_514),
+ (None, 0x050f00, darkmode.Variant.qt_515_0),
+ (None, 0x050f01, darkmode.Variant.qt_515_1),
+ (None, 0x050f02, darkmode.Variant.qt_515_2),
+ (None, 0x060000, darkmode.Variant.qt_515_2), # Qt 6
+])
+def test_variant(monkeypatch, qversion, webengine_version, expected):
+ monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion)
+ monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', webengine_version)
+ assert darkmode._variant() == expected
+
+
+@pytest.mark.parametrize('value, is_valid, expected', [
+ ('invalid_value', False, darkmode.Variant.qt_515_0),
+ ('qt_515_2', True, darkmode.Variant.qt_515_2),
+])
+def test_variant_override(monkeypatch, caplog, value, is_valid, expected):
+ monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: None)
+ monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', 0x050f00)
+ monkeypatch.setenv('QUTE_DARKMODE_VARIANT', value)
+
+ with caplog.at_level(logging.WARNING):
+ assert darkmode._variant() == expected
+
+ log_msg = 'Ignoring invalid QUTE_DARKMODE_VARIANT=invalid_value'
+ assert (log_msg in caplog.messages) != is_valid
+
+
+def test_broken_smart_images_policy(config_stub, monkeypatch, caplog):
+ config_stub.val.colors.webpage.darkmode.enabled = True
+ config_stub.val.colors.webpage.darkmode.policy.images = 'smart'
+ monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', 0x050f00)
+
+ with caplog.at_level(logging.WARNING):
+ settings = list(darkmode.settings())
+
+ assert caplog.messages[-1] == (
+ 'Ignoring colors.webpage.darkmode.policy.images = smart because of '
+ 'Qt 5.15.0 bug')
+
+ expected = [
+ [('darkModeEnabled', 'true')], # Qt 5.15
+ [('darkMode', '4')], # Qt 5.14
+ ]
+ assert settings in expected
+
+
+def test_new_chromium():
+ """Fail if we encounter an unknown Chromium version.
+
+ Dark mode in Chromium (or rather, the underlying Blink) is being changed with
+ almost every Chromium release.
+
+ Make this test fail deliberately with newer Chromium versions, so that
+ we can test whether dark mode still works manually, and adjust if not.
+ """
+ assert version._chromium_version() in [
+ 'unavailable', # QtWebKit
+ '61.0.3163.140', # Qt 5.10
+ '65.0.3325.230', # Qt 5.11
+ '69.0.3497.128', # Qt 5.12
+ '73.0.3683.105', # Qt 5.13
+ '77.0.3865.129', # Qt 5.14
+ '80.0.3987.163', # Qt 5.15.0
+ '83.0.4103.122', # Qt 5.15.2
+ ]
+
+
+def test_options(configdata_init):
+ """Make sure all darkmode options have the right attributes set."""
+ for name, opt in configdata.DATA.items():
+ if not name.startswith('colors.webpage.darkmode.'):
+ continue
+
+ assert not opt.supports_pattern, name
+ assert opt.restart, name
+
+ if opt.backends:
+ # On older Qt versions, this is an empty list.
+ assert opt.backends == [usertypes.Backend.QtWebEngine], name
+
+ if opt.raw_backends is not None:
+ assert not opt.raw_backends['QtWebKit'], name
+ assert opt.raw_backends['QtWebEngine'] == 'Qt 5.14', name
diff --git a/tests/unit/browser/webengine/test_spell.py b/tests/unit/browser/webengine/test_spell.py
index ab437a37d..f9f092605 100644
--- a/tests/unit/browser/webengine/test_spell.py
+++ b/tests/unit/browser/webengine/test_spell.py
@@ -22,10 +22,9 @@ import logging
import os
import pytest
-from PyQt5.QtCore import QLibraryInfo
from qutebrowser.browser.webengine import spell
-from qutebrowser.utils import usertypes, qtutils, standarddir
+from qutebrowser.utils import usertypes
def test_version(message_mock, caplog):
@@ -39,21 +38,6 @@ def test_version(message_mock, caplog):
assert msg.text == expected
-@pytest.mark.parametrize('qt_version, old, subdir', [
- ('5.9', True, 'global_datapath'),
- ('5.9', False, 'global_datapath'),
- ('5.10', True, 'global_datapath'),
- ('5.10', False, 'user_datapath'),
-])
-def test_dictionary_dir(monkeypatch, qt_version, old, subdir):
- monkeypatch.setattr(qtutils, 'qVersion', lambda: qt_version)
- monkeypatch.setattr(QLibraryInfo, 'location', lambda _: 'global_datapath')
- monkeypatch.setattr(standarddir, 'data', lambda: 'user_datapath')
-
- expected = os.path.join(subdir, 'qtwebengine_dictionaries')
- assert spell.dictionary_dir(old=old) == expected
-
-
def test_local_filename_dictionary_does_not_exist(monkeypatch):
"""Tests retrieving local filename when the dir doesn't exits."""
monkeypatch.setattr(
@@ -104,48 +88,9 @@ class TestInit:
monkeypatch.delenv(self.ENV, raising=False)
@pytest.fixture
- def patch_new_qt(self, monkeypatch):
- monkeypatch.setattr(spell.qtutils, 'version_check',
- lambda _ver, compiled: True)
-
- @pytest.fixture
def dict_dir(self, data_tmpdir):
return data_tmpdir / 'qtwebengine_dictionaries'
- @pytest.fixture
- def old_dict_dir(self, monkeypatch, tmpdir):
- data_dir = tmpdir / 'old'
- dict_dir = data_dir / 'qtwebengine_dictionaries'
- (dict_dir / 'somedict').ensure()
- monkeypatch.setattr(spell.QLibraryInfo, 'location',
- lambda _arg: str(data_dir))
- return dict_dir
-
- def test_old_qt(self, monkeypatch):
- monkeypatch.setattr(spell.qtutils, 'version_check',
- lambda _ver, compiled: False)
- spell.init()
- assert self.ENV not in os.environ
-
- def test_new_qt(self, dict_dir, patch_new_qt):
+ def test_init(self, dict_dir):
spell.init()
assert os.environ[self.ENV] == str(dict_dir)
-
- def test_moving(self, old_dict_dir, dict_dir, patch_new_qt):
- spell.init()
- assert (dict_dir / 'somedict').exists()
-
- def test_moving_oserror(self, mocker, caplog,
- old_dict_dir, dict_dir, patch_new_qt):
- mocker.patch('shutil.copytree', side_effect=OSError)
-
- with caplog.at_level(logging.ERROR):
- spell.init()
-
- assert caplog.messages[0] == 'Failed to copy old dictionaries'
-
- def test_moving_existing_destdir(self, old_dict_dir, dict_dir,
- patch_new_qt):
- dict_dir.ensure(dir=True)
- spell.init()
- assert not (dict_dir / 'somedict').exists()
diff --git a/tests/unit/browser/webengine/test_webengine_cookies.py b/tests/unit/browser/webengine/test_webengine_cookies.py
index 0933fa1c6..1ac9c5d1a 100644
--- a/tests/unit/browser/webengine/test_webengine_cookies.py
+++ b/tests/unit/browser/webengine/test_webengine_cookies.py
@@ -29,12 +29,9 @@ from qutebrowser.utils import urlmatch
@pytest.fixture
def filter_request():
- try:
- request = QWebEngineCookieStore.FilterRequest()
- request.firstPartyUrl = QUrl('https://example.com')
- return request
- except AttributeError:
- pytest.skip("FilterRequest not available")
+ request = QWebEngineCookieStore.FilterRequest()
+ request.firstPartyUrl = QUrl('https://example.com')
+ return request
@pytest.fixture(autouse=True)
@@ -85,15 +82,6 @@ def test_invalid_url(config_stub, filter_request, global_value):
assert cookies._accept_cookie(filter_request) == accepted
-def test_third_party_workaround(monkeypatch, config_stub, filter_request):
- monkeypatch.setattr(cookies.qtutils, 'version_check',
- lambda ver, compiled: False)
- config_stub.val.content.cookies.accept = 'no-3rdparty'
- filter_request.thirdParty = True
- filter_request.firstPartyUrl = QUrl()
- assert cookies._accept_cookie(filter_request)
-
-
@pytest.mark.parametrize('enabled', [True, False])
def test_logging(monkeypatch, config_stub, filter_request, caplog, enabled):
monkeypatch.setattr(cookies.objects, 'debug_flags',
@@ -117,12 +105,10 @@ class TestInstall:
profile = QWebEngineProfile()
cookies.install_filter(profile)
- @pytest.mark.parametrize('has_cookie_filter', [True, False])
- def test_fake_profile(self, stubs, has_cookie_filter):
- store = stubs.FakeCookieStore(has_cookie_filter=has_cookie_filter)
+ def test_fake_profile(self, stubs):
+ store = stubs.FakeCookieStore()
profile = stubs.FakeWebEngineProfile(cookie_store=store)
cookies.install_filter(profile)
- if has_cookie_filter:
- assert store.cookie_filter is cookies._accept_cookie
+ assert store.cookie_filter is cookies._accept_cookie
diff --git a/tests/unit/browser/webengine/test_webenginedownloads.py b/tests/unit/browser/webengine/test_webenginedownloads.py
index 3a830bd58..3749b01f5 100644
--- a/tests/unit/browser/webengine/test_webenginedownloads.py
+++ b/tests/unit/browser/webengine/test_webenginedownloads.py
@@ -24,7 +24,6 @@ import pytest
pytest.importorskip('PyQt5.QtWebEngineWidgets')
from qutebrowser.browser.webengine import webenginedownloads
-from helpers import utils
@pytest.mark.parametrize('path, expected', [
@@ -34,10 +33,8 @@ from helpers import utils
('foo - 1970-01-01T00:00:00.000Z', 'foo'),
('foo(a)', 'foo(a)'),
('foo1', 'foo1'),
- pytest.param('foo%20bar', 'foo bar', marks=utils.qt58),
- pytest.param('foo%2Fbar', 'bar', marks=utils.qt58),
- pytest.param('foo%20bar', 'foo%20bar', marks=utils.qt59),
- pytest.param('foo%2Fbar', 'foo%2Fbar', marks=utils.qt59),
+ ('foo%20bar', 'foo%20bar'),
+ ('foo%2Fbar', 'foo%2Fbar'),
])
def test_get_suggested_filename(path, expected):
assert webenginedownloads._get_suggested_filename(path) == expected
diff --git a/tests/unit/browser/webengine/test_webengineinterceptor.py b/tests/unit/browser/webengine/test_webengineinterceptor.py
index 2c352a6a8..7a4fd918a 100644
--- a/tests/unit/browser/webengine/test_webengineinterceptor.py
+++ b/tests/unit/browser/webengine/test_webengineinterceptor.py
@@ -27,8 +27,6 @@ pytest.importorskip('PyQt5.QtWebEngineWidgets')
from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInfo
from qutebrowser.browser.webengine import interceptor
-from qutebrowser.extensions import interceptors
-from qutebrowser.utils import qtutils
def test_no_missing_resource_types():
@@ -42,8 +40,4 @@ def test_no_missing_resource_types():
def test_resource_type_values():
request_interceptor = interceptor.RequestInterceptor()
for qt_value, qb_item in request_interceptor._resource_types.items():
- if (qtutils.version_check('5.7.1', exact=True, compiled=False) and
- qb_item == interceptors.ResourceType.unknown):
- # Qt 5.7 has ResourceTypeUnknown = 18 instead of 255
- continue
assert qt_value == qb_item.value
diff --git a/tests/unit/browser/webengine/test_webenginesettings.py b/tests/unit/browser/webengine/test_webenginesettings.py
index ec61df7c6..53f3e21b2 100644
--- a/tests/unit/browser/webengine/test_webenginesettings.py
+++ b/tests/unit/browser/webengine/test_webenginesettings.py
@@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
-import types
import logging
import pytest
@@ -25,7 +24,7 @@ import pytest
pytest.importorskip('PyQt5.QtWebEngineWidgets')
from qutebrowser.browser.webengine import webenginesettings
-from qutebrowser.utils import usertypes, qtutils
+from qutebrowser.utils import usertypes
from qutebrowser.misc import objects
@@ -34,8 +33,7 @@ def init(qapp, config_stub, cache_tmpdir, data_tmpdir, monkeypatch):
monkeypatch.setattr(webenginesettings.webenginequtescheme, 'init',
lambda: None)
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
- init_args = types.SimpleNamespace(enable_webengine_inspector=False)
- webenginesettings.init(init_args)
+ webenginesettings.init()
config_stub.changed.disconnect(webenginesettings._update_settings)
@@ -47,8 +45,6 @@ def test_big_cache_size(config_stub):
assert profile.httpCacheMaximumSize() == 2 ** 31 - 1
-@pytest.mark.skipif(
- not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
def test_non_existing_dict(config_stub, monkeypatch, message_mock, caplog):
monkeypatch.setattr(webenginesettings.spell, 'local_filename',
lambda _code: None)
@@ -63,8 +59,6 @@ def test_non_existing_dict(config_stub, monkeypatch, message_mock, caplog):
assert msg.text == expected
-@pytest.mark.skipif(
- not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
def test_existing_dict(config_stub, monkeypatch):
monkeypatch.setattr(webenginesettings.spell, 'local_filename',
lambda _code: 'en-US-8-0')
@@ -76,8 +70,6 @@ def test_existing_dict(config_stub, monkeypatch):
assert profile.spellCheckLanguages() == ['en-US-8-0']
-@pytest.mark.skipif(
- not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
def test_spell_check_disabled(config_stub, monkeypatch):
config_stub.val.spellcheck.languages = []
webenginesettings._update_settings('spellcheck.languages')
diff --git a/tests/unit/browser/webengine/test_webenginetab.py b/tests/unit/browser/webengine/test_webenginetab.py
index 4684921c6..02a034331 100644
--- a/tests/unit/browser/webengine/test_webenginetab.py
+++ b/tests/unit/browser/webengine/test_webenginetab.py
@@ -101,9 +101,6 @@ class TestWebengineScripts:
"""Make sure document-end is forced when needed."""
monkeypatch.setattr(greasemonkey.objects, 'backend',
usertypes.Backend.QtWebEngine)
- monkeypatch.setattr(greasemonkey.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- True)
scripts = [
greasemonkey.GreasemonkeyScript([
diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py
index f0aaf226e..9cd8eab9b 100644
--- a/tests/unit/browser/webkit/test_cache.py
+++ b/tests/unit/browser/webkit/test_cache.py
@@ -23,13 +23,6 @@ from PyQt5.QtCore import QUrl, QDateTime
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
from qutebrowser.browser.webkit import cache
-from qutebrowser.utils import qtutils
-
-
-pytestmark = pytest.mark.skipif(
- qtutils.version_check('5.7.1', compiled=False) and
- not qtutils.version_check('5.9', compiled=False),
- reason="QNetworkDiskCache is broken on Qt 5.7.1 and 5.8")
@pytest.fixture
diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py
index 8d4289f4b..a30bfe786 100644
--- a/tests/unit/browser/webkit/test_mhtml.py
+++ b/tests/unit/browser/webkit/test_mhtml.py
@@ -21,24 +21,16 @@
import io
import textwrap
import re
+import uuid
import pytest
mhtml = pytest.importorskip('qutebrowser.browser.webkit.mhtml')
-try:
- import cssutils
-except (ImportError, re.error):
- # Catching re.error because cssutils in earlier releases (<= 1.0) is
- # broken on Python 3.5
- # See https://bitbucket.org/cthedot/cssutils/issues/52
- cssutils = None
-
-
@pytest.fixture(autouse=True)
def patch_uuid(monkeypatch):
- monkeypatch.setattr("uuid.uuid4", lambda: "UUID")
+ monkeypatch.setattr(uuid, "uuid4", lambda: "UUID")
class Checker:
@@ -257,34 +249,25 @@ def test_empty_content_type(checker):
""")
-@pytest.mark.parametrize('has_cssutils', [
- pytest.param(True, marks=pytest.mark.skipif(
- cssutils is None, reason="requires cssutils"), id='with_cssutils'),
- pytest.param(False, id='no_cssutils'),
-])
-@pytest.mark.parametrize('inline, style, expected_urls', [
- pytest.param(False, "@import 'default.css'", ['default.css'],
+@pytest.mark.parametrize('style, expected_urls', [
+ pytest.param("@import 'default.css'", ['default.css'],
id='import with apostrophe'),
- pytest.param(False, '@import "default.css"', ['default.css'],
+ pytest.param('@import "default.css"', ['default.css'],
id='import with quote'),
- pytest.param(False, "@import \t 'tabbed.css'", ['tabbed.css'],
+ pytest.param("@import \t 'tabbed.css'", ['tabbed.css'],
id='import with tab'),
- pytest.param(False, "@import url('default.css')", ['default.css'],
+ pytest.param("@import url('default.css')", ['default.css'],
id='import with url()'),
- pytest.param(False, """body {
+ pytest.param("""body {
background: url("/bg-img.png")
}""", ['/bg-img.png'], id='background with body'),
- pytest.param(True, 'background: url(folder/file.png) no-repeat',
+ pytest.param('background: url(folder/file.png) no-repeat',
['folder/file.png'], id='background'),
- pytest.param(True, 'content: url()', [], id='content'),
+ pytest.param('content: url()', [], id='content'),
])
-def test_css_url_scanner(monkeypatch, has_cssutils, inline, style,
- expected_urls):
- if not has_cssutils:
- monkeypatch.setattr(mhtml, '_get_css_imports_cssutils',
- lambda data, inline=False: None)
+def test_css_url_scanner(monkeypatch, style, expected_urls):
expected_urls.sort()
- urls = mhtml._get_css_imports(style, inline=inline)
+ urls = mhtml._get_css_imports(style)
urls.sort()
assert urls == expected_urls
diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py
index 9325cb7d0..48e0c98fc 100644
--- a/tests/unit/browser/webkit/test_tabhistory.py
+++ b/tests/unit/browser/webkit/test_tabhistory.py
@@ -33,7 +33,8 @@ pytestmark = pytest.mark.qt_log_ignore('QIODevice::read.*: device not open')
ITEMS = [
Item(QUrl('https://www.heise.de/'), 'heise'),
- Item(QUrl('http://example.com/%E2%80%A6'), 'percent', active=True),
+ Item(QUrl('about:blank'), 'blank', active=True),
+ Item(QUrl('http://example.com/%E2%80%A6'), 'percent'),
Item(QUrl('http://example.com/?foo=bar'), 'arg',
original_url=QUrl('http://original.url.example.com/'),
user_data={'foo': 23, 'bar': 42}),
diff --git a/tests/unit/commands/test_argparser.py b/tests/unit/commands/test_argparser.py
index ccf81edd1..69119c4cf 100644
--- a/tests/unit/commands/test_argparser.py
+++ b/tests/unit/commands/test_argparser.py
@@ -28,7 +28,10 @@ from PyQt5.QtCore import QUrl
from qutebrowser.commands import argparser, cmdexc
-Enum = enum.Enum('Enum', ['foo', 'foo_bar'])
+class Enum(enum.Enum):
+
+ foo = enum.auto()
+ foo_bar = enum.auto()
class TestArgumentParser:
diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py
index 739b8b773..4732837a1 100644
--- a/tests/unit/completion/test_completiondelegate.py
+++ b/tests/unit/completion/test_completiondelegate.py
@@ -18,6 +18,8 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
from unittest import mock
+import hypothesis
+import hypothesis.strategies
import pytest
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextDocument, QColor
@@ -69,10 +71,17 @@ def test_benchmark_highlight(benchmark):
benchmark(bench)
+@hypothesis.given(text=hypothesis.strategies.text())
+def test_pattern_hypothesis(text):
+ """Make sure we can't produce invalid patterns."""
+ doc = QTextDocument()
+ completiondelegate._Highlighter(doc, text, Qt.red)
+
+
def test_highlighted(qtbot):
"""Make sure highlighting works.
- Note that with Qt 5.11.3 and > 5.12.1 we need to call setPlainText *after*
+ Note that with Qt > 5.12.1 we need to call setPlainText *after*
creating the highlighter for highlighting to work. Ideally, we'd test
whether CompletionItemDelegate._get_textdoc() works properly, but testing
that is kind of hard, so we just test it in isolation here.
diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py
index 356b854c5..c0ef4b47f 100644
--- a/tests/unit/completion/test_completionwidget.py
+++ b/tests/unit/completion/test_completionwidget.py
@@ -161,6 +161,7 @@ def test_completion_item_focus_no_model(which, completionview, model, qtbot):
completionview.completion_item_focus(which)
+@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.
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index e602f0ab6..8b4653b58 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -26,6 +26,8 @@ import time
from datetime import datetime
from unittest import mock
+import hypothesis
+import hypothesis.strategies
import pytest
from PyQt5.QtCore import QUrl, QDateTime
try:
@@ -37,7 +39,8 @@ except ImportError:
from qutebrowser.misc import objects
from qutebrowser.completion import completer
-from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
+from qutebrowser.completion.models import (
+ miscmodels, urlmodel, configmodel, listcategory)
from qutebrowser.config import configdata, configtypes
from qutebrowser.utils import usertypes
from qutebrowser.mainwindow import tabbedbrowser
@@ -124,7 +127,7 @@ def configdata_stub(config_stub, monkeypatch, configdata_init):
no_autoconfig=True)),
('bindings.commands', configdata.Option(
name='bindings.commands',
- description='Default keybindings',
+ description='Custom keybindings',
typ=configtypes.Dict(
keytype=configtypes.String(),
valtype=configtypes.Dict(
@@ -269,13 +272,38 @@ def test_help_completion(qtmodeltester, cmdutils_stub, key_config_stub,
(':tab-close', 'Close the current tab.', ''),
],
"Settings": [
- ('aliases', 'Aliases for commands.', None),
- ('bindings.commands', 'Default keybindings', None),
- ('bindings.default', 'Default keybindings', None),
- ('completion.open_categories', 'Which categories to show (in '
- 'which order) in the :open completion.', None),
- ('content.javascript.enabled', 'Enable/Disable JavaScript', None),
- ('url.searchengines', 'searchengines list', None),
+ (
+ 'aliases',
+ 'Aliases for commands.',
+ '{"q": "quit"}',
+ ),
+ (
+ 'bindings.commands',
+ 'Custom keybindings',
+ ('{"normal": {"<Ctrl+q>": "quit", "I": "invalid", "ZQ": "quit", '
+ '"d": "scroll down"}}'),
+ ),
+ (
+ 'bindings.default',
+ 'Default keybindings',
+ '{"normal": {"<Ctrl+q>": "quit", "d": "tab-close"}}',
+ ),
+ (
+ 'completion.open_categories',
+ 'Which categories to show (in which order) in the :open completion.',
+ '["searchengines", "quickmarks", "bookmarks", "history"]',
+ ),
+ (
+ 'content.javascript.enabled',
+ 'Enable/Disable JavaScript',
+ 'true'
+ ),
+ (
+ 'url.searchengines',
+ 'searchengines list',
+ ('{"DEFAULT": "https://duckduckgo.com/?q={}", '
+ '"google": "https://google.com/?q={}"}'),
+ ),
],
})
@@ -909,7 +937,7 @@ def test_setting_option_completion(qtmodeltester, config_stub,
_check_completions(model, {
"Options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
- ('bindings.commands', 'Default keybindings', (
+ ('bindings.commands', 'Custom keybindings', (
'{"normal": {"<Ctrl+q>": "quit", "I": "invalid", '
'"ZQ": "quit", "d": "scroll down"}}')),
('completion.open_categories', 'Which categories to show (in '
@@ -933,7 +961,7 @@ def test_setting_dict_option_completion(qtmodeltester, config_stub,
_check_completions(model, {
"Dict options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
- ('bindings.commands', 'Default keybindings', (
+ ('bindings.commands', 'Custom keybindings', (
'{"normal": {"<Ctrl+q>": "quit", "I": "invalid", '
'"ZQ": "quit", "d": "scroll down"}}')),
('url.searchengines', 'searchengines list',
@@ -1299,3 +1327,10 @@ def test_undo_completion(tabbed_browser_stubs, info):
"2020-01-01 00:00"),
],
})
+
+
+@hypothesis.given(text=hypothesis.strategies.text())
+def test_listcategory_hypothesis(text):
+ """Make sure we can't produce invalid patterns."""
+ cat = listcategory.ListCategory("test", [])
+ cat.set_pattern(text)
diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py
index 5718f6dc9..220aa40f7 100644
--- a/tests/unit/config/test_configcommands.py
+++ b/tests/unit/config/test_configcommands.py
@@ -212,14 +212,12 @@ class TestSet:
commands.set(win_id=0, option='foo?')
-@pytest.mark.parametrize('old', [True, False])
-def test_diff(commands, tabbed_browser_stubs, old):
+def test_diff(commands, tabbed_browser_stubs):
"""Run ':config-diff'.
Should open qute://configdiff."""
- commands.config_diff(win_id=0, old=old)
- url = QUrl('qute://configdiff/old') if old else QUrl('qute://configdiff')
- assert tabbed_browser_stubs[0].loaded_url == url
+ commands.config_diff(win_id=0)
+ assert tabbed_browser_stubs[0].loaded_url == QUrl('qute://configdiff')
class TestCycle:
@@ -499,7 +497,8 @@ class TestSource:
else:
assert False, location
- pyfile.write_text('c.content.javascript.enabled = False\n',
+ pyfile.write_text('\n'.join(['config.load_autoconfig(False)',
+ 'c.content.javascript.enabled = False']),
encoding='utf-8')
commands.config_source(arg, clear=clear)
@@ -511,14 +510,16 @@ class TestSource:
def test_config_py_arg_source(self, commands, config_py_arg, config_stub):
assert config_stub.val.content.javascript.enabled
- config_py_arg.write_text('c.content.javascript.enabled = False\n',
+ config_py_arg.write_text('\n'.join(['config.load_autoconfig(False)',
+ 'c.content.javascript.enabled = False']),
encoding='utf-8')
commands.config_source()
assert not config_stub.val.content.javascript.enabled
def test_errors(self, commands, config_tmpdir):
pyfile = config_tmpdir / 'config.py'
- pyfile.write_text('c.foo = 42', encoding='utf-8')
+ pyfile.write_text('\n'.join(['config.load_autoconfig(False)',
+ 'c.foo = 42']), encoding='utf-8')
with pytest.raises(cmdutils.CommandError) as excinfo:
commands.config_source()
@@ -529,7 +530,8 @@ class TestSource:
def test_invalid_source(self, commands, config_tmpdir):
pyfile = config_tmpdir / 'config.py'
- pyfile.write_text('1/0', encoding='utf-8')
+ pyfile.write_text('\n'.join(['config.load_autoconfig(False)',
+ '1/0']), encoding='utf-8')
with pytest.raises(cmdutils.CommandError) as excinfo:
commands.config_source()
@@ -571,7 +573,8 @@ class TestEdit:
def test_with_sourcing(self, commands, config_stub, patch_editor):
assert config_stub.val.content.javascript.enabled
- mock = patch_editor('c.content.javascript.enabled = False')
+ mock = patch_editor('\n'.join(['config.load_autoconfig(False)',
+ 'c.content.javascript.enabled = False']))
commands.config_edit()
@@ -580,16 +583,16 @@ class TestEdit:
def test_config_py_with_sourcing(self, commands, config_stub, patch_editor, config_py_arg):
assert config_stub.val.content.javascript.enabled
- conf = 'c.content.javascript.enabled = False'
- mock = patch_editor(conf)
+ conf = ['config.load_autoconfig(False)', 'c.content.javascript.enabled = False']
+ mock = patch_editor("\n".join(conf))
commands.config_edit()
mock.assert_called_once_with(unittest.mock.ANY)
assert not config_stub.val.content.javascript.enabled
- assert config_py_arg.read_text('utf-8').splitlines() == [conf]
+ assert config_py_arg.read_text('utf-8').splitlines() == conf
def test_error(self, commands, config_stub, patch_editor, message_mock,
caplog):
- patch_editor('c.foo = 42')
+ patch_editor('\n'.join(['config.load_autoconfig(False)', 'c.foo = 42']))
with caplog.at_level(logging.ERROR):
commands.config_edit()
diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py
index 4ea5ffe6d..ae17cd51b 100644
--- a/tests/unit/config/test_configdata.py
+++ b/tests/unit/config/test_configdata.py
@@ -289,7 +289,7 @@ class TestParseYamlBackend:
data = self._yaml("""
backend:
QtWebKit: {}
- QtWebEngine: Qt 5.8
+ QtWebEngine: Qt 5.15
""".format('true' if webkit else 'false'))
monkeypatch.setattr(configdata.qtutils, 'version_check',
lambda v: has_new_version)
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
index 27e96ef7d..11808e2c2 100644
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -28,7 +28,7 @@ from PyQt5.QtCore import QSettings
from qutebrowser.config import (config, configfiles, configexc, configdata,
configtypes)
-from qutebrowser.utils import utils, usertypes, urlmatch
+from qutebrowser.utils import utils, usertypes, urlmatch, standarddir
from qutebrowser.keyinput import keyutils
@@ -659,6 +659,7 @@ class ConfPy:
def __init__(self, tmpdir, filename: str = "config.py"):
self._file = tmpdir / filename
self.filename = str(self._file)
+ config.instance.warn_autoconfig = False
def write(self, *lines):
text = '\n'.join(lines)
@@ -954,6 +955,19 @@ class TestConfigPy:
# points at the location *after* the +
assert " ^" in tblines or " ^" in tblines
+ def test_load_autoconfig_warning(self, confpy):
+ confpy.write('')
+ config.instance.warn_autoconfig = True
+ with pytest.raises(configexc.ConfigFileErrors) as excinfo:
+ configfiles.read_config_py(confpy.filename)
+ assert len(excinfo.value.errors) == 1
+ error = excinfo.value.errors[0]
+ assert error.text == "autoconfig loading not specified"
+ exception_text = ('Your config.py should call either `config.load_autoconfig()`'
+ ' (to load settings configured via the GUI) or '
+ '`config.load_autoconfig(False)` (to not do so)')
+ assert str(error.exception) == exception_text
+
def test_unhandled_exception(self, confpy):
confpy.write("1/0")
error = confpy.read(error=True)
@@ -1064,6 +1078,24 @@ class TestConfigPy:
assert not config.instance.get_obj('content.javascript.enabled')
+ def test_source_configpy_arg(self, tmpdir, data_tmpdir, monkeypatch):
+ alt_filename = 'alt-config.py'
+
+ alt_confpy_dir = tmpdir / 'alt-confpy-dir'
+ alt_confpy_dir.ensure(dir=True)
+ monkeypatch.setattr(standarddir, 'config_py',
+ lambda: str(alt_confpy_dir / alt_filename))
+
+ subfile = alt_confpy_dir / 'subfile.py'
+ subfile.write_text("c.content.javascript.enabled = False",
+ encoding='utf-8')
+
+ alt_confpy = ConfPy(alt_confpy_dir, alt_filename)
+ alt_confpy.write("config.source('subfile.py')")
+ alt_confpy.read()
+
+ assert not config.instance.get_obj('content.javascript.enabled')
+
def test_source_errors(self, tmpdir, confpy):
subfile = tmpdir / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')
@@ -1126,8 +1158,8 @@ class TestConfigPyWriter:
# qute://help/configuring.html
# qute://help/settings.html
- # Uncomment this to still load settings configured via autoconfig.yml
- # config.load_autoconfig()
+ # Change the argument to True to still load settings configured via autoconfig.yml
+ config.load_autoconfig(False)
# This is an option description. Nullam eu ante vel est convallis
# dignissim. Fusce suscipit, wisi nec facilisis facilisis, est dui
@@ -1179,7 +1211,7 @@ class TestConfigPyWriter:
lines = list(writer._gen_lines())
assert "## Autogenerated config.py" in lines
- assert "# config.load_autoconfig()" in lines
+ assert "# config.load_autoconfig(True)" in lines
assert "# c.opt = 'val'" in lines
assert "## Bindings for normal mode" in lines
assert "# config.bind(',x', 'message-info normal')" in lines
@@ -1228,7 +1260,7 @@ class TestConfigPyWriter:
commented=False)
lines = list(writer._gen_lines())
assert lines[0] == '# Autogenerated config.py'
- assert lines[-2] == '# config.load_autoconfig()'
+ assert lines[-2] == 'config.load_autoconfig(False)'
assert not lines[-1]
def test_pattern(self):
diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py
index 8381456e1..23cc890e4 100644
--- a/tests/unit/config/test_configinit.py
+++ b/tests/unit/config/test_configinit.py
@@ -18,6 +18,7 @@
"""Tests for qutebrowser.config.configinit."""
+import builtins
import logging
import unittest.mock
@@ -63,7 +64,8 @@ def configdata_init(monkeypatch):
class TestEarlyInit:
def test_config_py_path(self, args, init_patch, config_py_arg):
- config_py_arg.write('c.colors.hints.bg = "red"\n')
+ config_py_arg.write('\n'.join(['config.load_autoconfig()',
+ 'c.colors.hints.bg = "red"']))
configinit.early_init(args)
expected = 'colors.hints.bg = red'
assert config.instance.dump_userconfig() == expected
@@ -75,7 +77,8 @@ class TestEarlyInit:
config_py_file = config_tmpdir / 'config.py'
if config_py:
- config_py_lines = ['c.colors.hints.bg = "red"']
+ config_py_lines = ['c.colors.hints.bg = "red"',
+ 'config.load_autoconfig(False)']
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
@@ -108,7 +111,7 @@ class TestEarlyInit:
expected = '<Default configuration>'
assert config.instance.dump_userconfig() == expected
- @pytest.mark.parametrize('load_autoconfig', [True, False]) # noqa
+ @pytest.mark.parametrize('load_autoconfig', [True, False])
@pytest.mark.parametrize('config_py', [True, 'error', False])
@pytest.mark.parametrize('invalid_yaml', ['42', 'list', 'unknown',
'wrong-type', False])
@@ -147,8 +150,7 @@ class TestEarlyInit:
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
- if load_autoconfig:
- config_py_lines.append('config.load_autoconfig()')
+ config_py_lines.append('config.load_autoconfig({})'.format(load_autoconfig))
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
@@ -310,6 +312,7 @@ class TestLateInit:
elif method == 'py':
config_py_file = config_tmpdir / 'config.py'
lines = ["c.{} = '{}'".format(k, v) for k, v in settings]
+ lines.append("config.load_autoconfig(False)")
config_py_file.write_text('\n'.join(lines), 'utf-8', ensure=True)
configinit.early_init(args)
@@ -388,6 +391,6 @@ def test_get_backend(monkeypatch, args, config_stub,
args.backend = arg
config_stub.val.backend = confval
- monkeypatch.setattr('builtins.__import__', fake_import)
+ monkeypatch.setattr(builtins, '__import__', fake_import)
assert configinit.get_backend(args) == used
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index a98584164..710018604 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -19,6 +19,7 @@
"""Tests for qutebrowser.config.configtypes."""
import re
+import sys
import json
import math
import warnings
@@ -137,10 +138,16 @@ class TestValidValues:
def test_descriptions(self, klass):
"""Test descriptions."""
- vv = klass(('foo', "foo desc"), ('bar', "bar desc"), 'baz')
- assert vv.descriptions['foo'] == "foo desc"
- assert vv.descriptions['bar'] == "bar desc"
- assert 'baz' not in vv.descriptions
+ vv = klass(
+ ('one-with', "desc 1"),
+ ('two-with', "desc 2"),
+ 'three-without',
+ ('four-without', None)
+ )
+ assert vv.descriptions['one-with'] == "desc 1"
+ assert vv.descriptions['two-with'] == "desc 2"
+ assert 'three-without' not in vv.descriptions
+ assert 'four-without' not in vv.descriptions
@pytest.mark.parametrize('args, expected', [
(['a', 'b'], "<qutebrowser.config.configtypes.ValidValues "
@@ -248,15 +255,11 @@ class TestAll:
configtypes.PercOrInt, # ditto
]:
return
- elif (isinstance(typ, functools.partial) and
- isinstance(typ.func, (configtypes.ListOrValue,
- configtypes.List))):
+ elif (isinstance(klass, functools.partial) and klass.func in [
+ configtypes.ListOrValue, configtypes.List, configtypes.Dict]):
# ListOrValue: "- /" -> "/"
# List: "- /" -> ["/"]
- return
- elif (isinstance(typ, configtypes.ListOrValue) and
- isinstance(typ.valtype, configtypes.Int)):
- # "00" -> "0"
+ # Dict: '{":": "A"}' -> ':: A'
return
assert converted == s
@@ -399,14 +402,11 @@ class MappingSubclass(configtypes.MappingType):
"""A MappingType we use in TestMappingType which is valid/good."""
MAPPING = {
- 'one': 1,
- 'two': 2,
+ 'one': (1, 'one doc'),
+ 'two': (2, 'two doc'),
+ 'three': (3, None),
}
- def __init__(self, none_ok=False):
- super().__init__(none_ok)
- self.valid_values = configtypes.ValidValues('one', 'two')
-
class TestMappingType:
@@ -432,11 +432,12 @@ class TestMappingType:
def test_to_str(self, klass):
assert klass().to_str('one') == 'one'
- @pytest.mark.parametrize('typ', [configtypes.ColorSystem(),
- configtypes.Position(),
- configtypes.SelectOnRemove()])
- def test_mapping_type_matches_valid_values(self, typ):
- assert sorted(typ.MAPPING) == sorted(typ.valid_values)
+ def test_valid_values(self, klass):
+ assert klass().valid_values == configtypes.ValidValues(
+ ('one', 'one doc'),
+ ('two', 'two doc'),
+ ('three', None),
+ )
class TestString:
@@ -1487,27 +1488,19 @@ class TestRegex:
@pytest.mark.parametrize('val', [
pytest.param(r'(foo|bar))?baz[fis]h', id='unmatched parens'),
pytest.param('(' * 500, id='too many parens'),
+ pytest.param(r'foo\Xbar', id='invalid escape X'),
+ pytest.param(r'foo\Cbar', id='invalid escape C'),
+ pytest.param(r'[[]]', id='nested set', marks=pytest.mark.skipif(
+ sys.hexversion < 0x03070000,
+ reason="Warning was added in Python 3.7")),
+ pytest.param(r'[a||b]', id='set operation', marks=pytest.mark.skipif(
+ sys.hexversion < 0x03070000,
+ reason="Warning was added in Python 3.7")),
])
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().to_py(val)
- @pytest.mark.parametrize('val', [
- r'foo\Xbar',
- r'foo\Cbar',
- ])
- def test_to_py_maybe_valid(self, klass, val):
- """Those values are valid on some Python versions (and systems?).
-
- On others, they raise a DeprecationWarning because of an invalid
- escape. This tests makes sure this gets translated to a
- ValidationError.
- """
- try:
- klass().to_py(val)
- except configexc.ValidationError:
- pass
-
@pytest.mark.parametrize('warning', [
Warning('foo'), DeprecationWarning('foo'),
])
@@ -1523,20 +1516,6 @@ class TestRegex:
with pytest.raises(type(warning)):
regex.to_py('foo')
- def test_bad_pattern_warning(self, mocker, klass):
- """Test a simulated bad pattern warning.
-
- This only seems to happen with Python 3.5, so we simulate this for
- better coverage.
- """
- regex = klass()
- m = mocker.patch('qutebrowser.config.configtypes.re')
- m.compile.side_effect = lambda *args: warnings.warn(r'bad escape \C',
- DeprecationWarning)
- m.error = re.error
- with pytest.raises(configexc.ValidationError):
- regex.to_py('foo')
-
@pytest.mark.parametrize('flags, expected', [
(None, 0),
('IGNORECASE', re.IGNORECASE),
@@ -1974,7 +1953,7 @@ class TestFuzzyUrl:
assert klass().to_py(val) == expected
@pytest.mark.parametrize('val', [
- '::foo', # invalid URL
+ '', # invalid URL
'foo bar', # invalid search term
])
def test_to_py_invalid(self, klass, val):
diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py
index 7e1a7c744..4830340cf 100644
--- a/tests/unit/config/test_configutils.py
+++ b/tests/unit/config/test_configutils.py
@@ -21,6 +21,7 @@ import hypothesis
from hypothesis import strategies
import pytest
from PyQt5.QtCore import QUrl
+from PyQt5.QtWidgets import QLabel
from qutebrowser.config import configutils, configdata, configtypes, configexc
from qutebrowser.utils import urlmatch, usertypes, qtutils
@@ -364,3 +365,45 @@ class TestFontFamilies:
assert family
str(families)
+
+ def test_system_default_basics(self, qapp):
+ families = configutils.FontFamilies.from_system_default()
+ assert len(families) == 1
+ assert str(families)
+
+ def test_system_default_rendering(self, qtbot):
+ families = configutils.FontFamilies.from_system_default()
+
+ label = QLabel()
+ qtbot.add_widget(label)
+ label.setText("Hello World")
+
+ stylesheet = f'font-family: {families.to_str(quote=True)}'
+ print(stylesheet)
+ label.setStyleSheet(stylesheet)
+
+ with qtbot.waitExposed(label):
+ # Needed so the font gets calculated
+ label.show()
+ info = label.fontInfo()
+
+ # Check the requested font to make sure CSS parsing worked
+ assert label.font().family() == families.family
+
+ # Try to find out whether the monospace font did a fallback on a non-monospace
+ # font...
+ fallback_label = QLabel()
+ qtbot.add_widget(label)
+ fallback_label.setText("fallback")
+
+ with qtbot.waitExposed(fallback_label):
+ # Needed so the font gets calculated
+ fallback_label.show()
+
+ fallback_family = fallback_label.fontInfo().family()
+ print(f'fallback: {fallback_family}')
+ if info.family() == fallback_family:
+ 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 0b3cc7c2b..b050113b4 100644
--- a/tests/unit/config/test_qtargs.py
+++ b/tests/unit/config/test_qtargs.py
@@ -22,8 +22,8 @@ import os
import pytest
from qutebrowser import qutebrowser
-from qutebrowser.config import qtargs, configdata
-from qutebrowser.utils import usertypes, version
+from qutebrowser.config import qtargs
+from qutebrowser.utils import usertypes
from helpers import utils
@@ -42,8 +42,7 @@ class TestQtArgs:
@pytest.fixture(autouse=True)
def reduce_args(self, monkeypatch, config_stub):
"""Make sure no --disable-shared-workers/referer argument get added."""
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, compiled=False: True)
+ monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: '5.15.0')
config_stub.val.content.headers.referer = 'always'
@pytest.mark.parametrize('args, expected', [
@@ -95,8 +94,7 @@ class TestQtArgs:
])
def test_shared_workers(self, config_stub, monkeypatch, parser,
backend, expected):
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, compiled=False: False)
+ monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: '5.14.0')
monkeypatch.setattr(qtargs.objects, 'backend', backend)
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
@@ -118,7 +116,7 @@ class TestQtArgs:
def test_in_process_stack_traces(self, monkeypatch, parser, backend,
version_check, debug_flag, expected):
monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, compiled=False: version_check)
+ lambda version, compiled=False, exact=False: version_check)
monkeypatch.setattr(qtargs.objects, 'backend', backend)
parsed = parser.parse_args(['--debug-flag', 'stack'] if debug_flag
else [])
@@ -134,18 +132,24 @@ class TestQtArgs:
assert '--disable-in-process-stack-traces' in args
assert '--enable-in-process-stack-traces' not in args
- @pytest.mark.parametrize('flags, added', [
- ([], False),
- (['--debug-flag', 'chromium'], True),
+ @pytest.mark.parametrize('flags, args', [
+ ([], []),
+ (['--debug-flag', 'chromium'], ['--enable-logging', '--v=1']),
+ (['--debug-flag', 'wait-renderer-process'], ['--renderer-startup-dialog']),
])
- def test_chromium_debug(self, monkeypatch, parser, flags, added):
+ def test_chromium_flags(self, monkeypatch, parser, flags, args):
monkeypatch.setattr(qtargs.objects, 'backend',
usertypes.Backend.QtWebEngine)
parsed = parser.parse_args(flags)
args = qtargs.qt_args(parsed)
- for arg in ['--enable-logging', '--v=1']:
- assert (arg in args) == added
+ if args:
+ for arg in args:
+ assert arg in args
+ else:
+ assert '--enable-logging' not in args
+ assert '--v=1' not in args
+ assert '--renderer-startup-dialog' not in args
@pytest.mark.parametrize('config, added', [
('none', False),
@@ -162,25 +166,6 @@ class TestQtArgs:
args = qtargs.qt_args(parsed)
assert ('--disable-gpu' in args) == added
- @utils.qt510
- @pytest.mark.parametrize('new_version, autoplay, added', [
- (True, False, False), # new enough to not need it
- (False, True, False), # autoplay enabled
- (False, False, True),
- ])
- def test_autoplay(self, config_stub, monkeypatch, parser,
- new_version, autoplay, added):
- monkeypatch.setattr(qtargs.objects, 'backend',
- usertypes.Backend.QtWebEngine)
- config_stub.val.content.autoplay = autoplay
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, compiled=False: new_version)
-
- parsed = parser.parse_args([])
- args = qtargs.qt_args(parsed)
- assert ('--autoplay-policy=user-gesture-required' in args) == added
-
- @utils.qt59
@pytest.mark.parametrize('policy, arg', [
('all-interfaces', None),
@@ -267,14 +252,31 @@ class TestQtArgs:
else:
assert arg in args
- @pytest.mark.parametrize('referer, arg', [
- ('always', None),
- ('never', '--no-referrers'),
- ('same-domain', '--reduced-referrer-granularity'),
+ @pytest.mark.parametrize('qt_version, referer, arg', [
+ # 'always' -> no arguments
+ ('5.15.0', 'always', None),
+
+ # 'never' is handled via interceptor for most Qt versions
+ ('5.12.3', 'never', '--no-referrers'),
+ ('5.12.4', 'never', None),
+ ('5.13.0', 'never', '--no-referrers'),
+ ('5.13.1', 'never', None),
+ ('5.14.0', 'never', None),
+ ('5.15.0', 'never', None),
+
+ # 'same-domain' - arguments depend on Qt versions
+ ('5.13.0', 'same-domain', '--reduced-referrer-granularity'),
+ ('5.14.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'),
+ ('5.15.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'),
])
- def test_referer(self, config_stub, monkeypatch, parser, referer, arg):
- monkeypatch.setattr(qtargs.objects, 'backend',
- usertypes.Backend.QtWebEngine)
+ def test_referer(self, config_stub, monkeypatch, parser, qt_version, referer, arg):
+ monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine)
+ monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: qt_version)
+
+ # Avoid WebRTC pipewire feature
+ monkeypatch.setattr(qtargs.utils, 'is_linux', False)
+ # Avoid overlay scrollbar feature
+ config_stub.val.scrolling.bar = 'never'
config_stub.val.content.headers.referer = referer
parsed = parser.parse_args([])
@@ -283,23 +285,29 @@ class TestQtArgs:
if arg is None:
assert '--no-referrers' not in args
assert '--reduced-referrer-granularity' not in args
+ assert '--enable-features=ReducedReferrerGranularity' not in args
else:
assert arg in args
- @pytest.mark.parametrize('dark, new_qt, added', [
- (True, True, True),
- (True, False, False),
- (False, True, False),
- (False, False, False),
+ @pytest.mark.parametrize('dark, qt_version, added', [
+ (True, "5.13", False), # not supported
+ (True, "5.14", True),
+ (True, "5.15.0", True),
+ (True, "5.15.1", True),
+ (True, "5.15.2", False), # handled via blink setting
+
+ (False, "5.13", False),
+ (False, "5.14", False),
+ (False, "5.15.0", False),
+ (False, "5.15.1", False),
+ (False, "5.15.2", False),
])
@utils.qt514
def test_prefers_color_scheme_dark(self, config_stub, monkeypatch, parser,
- dark, new_qt, added):
+ dark, qt_version, added):
monkeypatch.setattr(qtargs.objects, 'backend',
usertypes.Backend.QtWebEngine)
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- new_qt)
+ monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: qt_version)
config_stub.val.colors.webpage.prefers_color_scheme_dark = dark
@@ -308,26 +316,20 @@ class TestQtArgs:
assert ('--force-dark-mode' in args) == added
- @pytest.mark.parametrize('bar, new_qt, is_mac, added', [
+ @pytest.mark.parametrize('bar, is_mac, added', [
# Overlay bar enabled
- ('overlay', True, False, True),
+ ('overlay', False, True),
# No overlay on mac
- ('overlay', True, True, False),
- ('overlay', False, True, False),
- # No overlay on old Qt
- ('overlay', False, False, False),
+ ('overlay', True, False),
# Overlay disabled
- ('when-searching', True, False, False),
- ('always', True, False, False),
- ('never', True, False, False),
+ ('when-searching', False, False),
+ ('always', False, False),
+ ('never', False, False),
])
def test_overlay_scrollbar(self, config_stub, monkeypatch, parser,
- bar, new_qt, is_mac, added):
+ bar, is_mac, added):
monkeypatch.setattr(qtargs.objects, 'backend',
usertypes.Backend.QtWebEngine)
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- new_qt)
monkeypatch.setattr(qtargs.utils, 'is_mac', is_mac)
# Avoid WebRTC pipewire feature
monkeypatch.setattr(qtargs.utils, 'is_linux', False)
@@ -381,125 +383,22 @@ class TestQtArgs:
assert combined_flag in args
assert overlay_flag not in args
- @utils.qt514
def test_blink_settings(self, config_stub, monkeypatch, parser):
+ from qutebrowser.browser.webengine import darkmode
monkeypatch.setattr(qtargs.objects, 'backend',
usertypes.Backend.QtWebEngine)
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- True)
+ monkeypatch.setattr(darkmode, '_variant',
+ lambda: darkmode.Variant.qt_515_2)
config_stub.val.colors.webpage.darkmode.enabled = True
parsed = parser.parse_args([])
args = qtargs.qt_args(parsed)
- assert '--blink-settings=darkModeEnabled=true' in args
-
-
-class TestDarkMode:
+ expected = ('--blink-settings=forceDarkModeEnabled=true,'
+ 'forceDarkModeImagePolicy=2')
- pytestmark = utils.qt514
-
- @pytest.fixture(autouse=True)
- def patch_backend(self, monkeypatch):
- monkeypatch.setattr(qtargs.objects, 'backend',
- usertypes.Backend.QtWebEngine)
-
- @pytest.mark.parametrize('settings, new_qt, expected', [
- # Disabled
- ({}, True, []),
- ({}, False, []),
-
- # Enabled without customization
- (
- {'enabled': True},
- True,
- [('darkModeEnabled', 'true')]
- ),
- (
- {'enabled': True},
- False,
- [('darkMode', '4')]
- ),
-
- # Algorithm
- (
- {'enabled': True, 'algorithm': 'brightness-rgb'},
- True,
- [('darkModeEnabled', 'true'),
- ('darkModeInversionAlgorithm', '2')],
- ),
- (
- {'enabled': True, 'algorithm': 'brightness-rgb'},
- False,
- [('darkMode', '2')],
- ),
-
- ])
- def test_basics(self, config_stub, monkeypatch,
- settings, new_qt, expected):
- for k, v in settings.items():
- config_stub.set_obj('colors.webpage.darkmode.' + k, v)
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- new_qt)
-
- assert list(qtargs._darkmode_settings()) == expected
-
- @pytest.mark.parametrize('setting, value, exp_key, exp_val', [
- ('contrast', -0.5,
- 'darkModeContrast', '-0.5'),
- ('policy.page', 'smart',
- 'darkModePagePolicy', '1'),
- ('policy.images', 'smart',
- 'darkModeImagePolicy', '2'),
- ('threshold.text', 100,
- 'darkModeTextBrightnessThreshold', '100'),
- ('threshold.background', 100,
- 'darkModeBackgroundBrightnessThreshold', '100'),
- ('grayscale.all', True,
- 'darkModeGrayscale', 'true'),
- ('grayscale.images', 0.5,
- 'darkModeImageGrayscale', '0.5'),
- ])
- def test_customization(self, config_stub, monkeypatch,
- setting, value, exp_key, exp_val):
- config_stub.val.colors.webpage.darkmode.enabled = True
- config_stub.set_obj('colors.webpage.darkmode.' + setting, value)
- monkeypatch.setattr(qtargs.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- True)
-
- expected = [('darkModeEnabled', 'true'), (exp_key, exp_val)]
- assert list(qtargs._darkmode_settings()) == expected
-
- def test_new_chromium(self):
- """Fail if we encounter an unknown Chromium version.
-
- Dark mode in Chromium currently is undergoing various changes (as it's
- relatively recent), and Qt 5.15 is supposed to update the underlying
- Chromium at some point.
-
- Make this test fail deliberately with newer Chromium versions, so that
- we can test whether dark mode still works manually, and adjust if not.
- """
- assert version._chromium_version() in [
- 'unavailable', # QtWebKit
- '77.0.3865.129', # Qt 5.14
- '80.0.3987.163', # Qt 5.15
- ]
-
- def test_options(self, configdata_init):
- """Make sure all darkmode options have the right attributes set."""
- for name, opt in configdata.DATA.items():
- if not name.startswith('colors.webpage.darkmode.'):
- continue
-
- backends = {'QtWebEngine': 'Qt 5.14', 'QtWebKit': False}
- assert not opt.supports_pattern, name
- assert opt.restart, name
- assert opt.raw_backends == backends, name
+ assert expected in args
class TestEnvVars:
diff --git a/tests/unit/config/test_websettings.py b/tests/unit/config/test_websettings.py
index 651c14aba..e1445efbb 100644
--- a/tests/unit/config/test_websettings.py
+++ b/tests/unit/config/test_websettings.py
@@ -95,10 +95,10 @@ def test_user_agent(monkeypatch, config_stub, qapp):
def test_config_init(request, monkeypatch, config_stub):
if request.config.webengine:
from qutebrowser.browser.webengine import webenginesettings
- monkeypatch.setattr(webenginesettings, 'init', lambda _args: None)
+ monkeypatch.setattr(webenginesettings, 'init', lambda: None)
else:
from qutebrowser.browser.webkit import webkitsettings
- monkeypatch.setattr(webkitsettings, 'init', lambda _args: None)
+ monkeypatch.setattr(webkitsettings, 'init', lambda: None)
websettings.init(args=None)
assert config_stub.dump_userconfig() == '<Default configuration>'
diff --git a/tests/unit/javascript/stylesheet/test_appendchild.js b/tests/unit/javascript/stylesheet/test_appendchild.js
index d1deadba6..aa1294cb3 100644
--- a/tests/unit/javascript/stylesheet/test_appendchild.js
+++ b/tests/unit/javascript/stylesheet/test_appendchild.js
@@ -9,37 +9,37 @@ var iframe, object;
// svg iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '1' };
-iframe.src = "svg.xml";
+// iframe.src = "svg.xml";
kungFuDeathGrip.appendChild(iframe);
// object iframe
object = document.createElement('object');
object.onload = function () { kungFuDeathGrip.title += '2' };
-object.data = "svg.xml";
+// object.data = "svg.xml";
kungFuDeathGrip.appendChild(object);
// xml iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '3' };
-iframe.src = "empty.xml";
+// iframe.src = "empty.xml";
kungFuDeathGrip.appendChild(iframe);
// html iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '4' };
-iframe.src = "empty.html";
+// iframe.src = "empty.html";
kungFuDeathGrip.appendChild(iframe);
// html iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '5' };
-iframe.src = "xhtml.1";
+// iframe.src = "xhtml.1";
kungFuDeathGrip.appendChild(iframe);
// html iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '6' };
-iframe.src = "xhtml.2";
+// iframe.src = "xhtml.2";
kungFuDeathGrip.appendChild(iframe);
// html iframe
iframe = document.createElement('iframe');
iframe.onload = function () { kungFuDeathGrip.title += '7' };
-iframe.src = "xhtml.3";
+// iframe.src = "xhtml.3";
kungFuDeathGrip.appendChild(iframe);
// add the lot to the document
diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py
index c066ab479..5f2c94b56 100644
--- a/tests/unit/javascript/test_greasemonkey.py
+++ b/tests/unit/javascript/test_greasemonkey.py
@@ -27,6 +27,7 @@ from PyQt5.QtCore import QUrl
from qutebrowser.utils import usertypes
from qutebrowser.browser import greasemonkey
+from qutebrowser.misc import objects
test_gm_script = r"""
// ==UserScript==
@@ -40,12 +41,15 @@ test_gm_script = r"""
console.log("Script is running.");
"""
-pytestmark = pytest.mark.usefixtures('data_tmpdir')
+pytestmark = [
+ pytest.mark.usefixtures('data_tmpdir'),
+ pytest.mark.usefixtures('config_tmpdir')
+]
def _save_script(script_text, filename):
# pylint: disable=no-member
- file_path = py.path.local(greasemonkey._scripts_dir()) / filename
+ file_path = py.path.local(greasemonkey._scripts_dirs()[0]) / filename
# pylint: enable=no-member
file_path.write_text(script_text, encoding='utf-8', ensure=True)
@@ -168,15 +172,6 @@ def test_utf8_bom():
class TestForceDocumentEnd:
- @pytest.fixture
- def patch(self, monkeypatch):
- def _patch(*, backend, qt_512):
- monkeypatch.setattr(greasemonkey.objects, 'backend', backend)
- monkeypatch.setattr(greasemonkey.qtutils, 'version_check',
- lambda version, exact=False, compiled=True:
- qt_512)
- return _patch
-
def _get_script(self, *, namespace, name):
source = textwrap.dedent("""
// ==UserScript==
@@ -192,26 +187,15 @@ class TestForceDocumentEnd:
assert len(scripts) == 1
return scripts[0]
- @pytest.mark.parametrize('backend, qt_512', [
- (usertypes.Backend.QtWebKit, True),
- (usertypes.Backend.QtWebEngine, False),
- ])
- def test_not_applicable(self, patch, backend, qt_512):
- """Test backend/Qt version combinations which don't need a fix."""
- patch(backend=backend, qt_512=qt_512)
- script = self._get_script(namespace='https://github.com/ParticleCore',
- name='Iridium')
- assert not script.needs_document_end_workaround()
-
@pytest.mark.parametrize('namespace, name, force', [
('http://userstyles.org', 'foobar', True),
('https://github.com/ParticleCore', 'Iridium', True),
('https://github.com/ParticleCore', 'Foo', False),
('https://example.org', 'Iridium', False),
])
- def test_matching(self, patch, namespace, name, force):
+ def test_matching(self, monkeypatch, namespace, name, force):
"""Test matching based on namespace/name."""
- patch(backend=usertypes.Backend.QtWebEngine, qt_512=True)
+ monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
script = self._get_script(namespace=namespace, name=name)
assert script.needs_document_end_workaround() == force
diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py
index f8caaf1af..cd57d33cb 100644
--- a/tests/unit/keyinput/test_basekeyparser.py
+++ b/tests/unit/keyinput/test_basekeyparser.py
@@ -25,7 +25,7 @@ from PyQt5.QtCore import Qt
import pytest
from qutebrowser.keyinput import basekeyparser, keyutils
-from qutebrowser.utils import utils, usertypes
+from qutebrowser.utils import usertypes
# Alias because we need this a lot in here.
@@ -119,11 +119,9 @@ def test_read_config(keyparser, key_config_stub, changed_mode, expected):
class TestHandle:
def test_valid_key(self, prompt_keyparser, handle_text):
- modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
-
infos = [
- keyutils.KeyInfo(Qt.Key_A, modifier),
- keyutils.KeyInfo(Qt.Key_X, modifier),
+ keyutils.KeyInfo(Qt.Key_A, Qt.ControlModifier),
+ keyutils.KeyInfo(Qt.Key_X, Qt.ControlModifier),
]
for info in infos:
prompt_keyparser.handle(info.to_event())
@@ -133,11 +131,9 @@ class TestHandle:
assert not prompt_keyparser._sequence
def test_valid_key_count(self, prompt_keyparser):
- modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
-
infos = [
keyutils.KeyInfo(Qt.Key_5, Qt.NoModifier),
- keyutils.KeyInfo(Qt.Key_A, modifier),
+ keyutils.KeyInfo(Qt.Key_A, Qt.ControlModifier),
]
for info in infos:
prompt_keyparser.handle(info.to_event())
diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py
index 0df721c68..4c297ef7e 100644
--- a/tests/unit/keyinput/test_keyutils.py
+++ b/tests/unit/keyinput/test_keyutils.py
@@ -28,7 +28,6 @@ from PyQt5.QtWidgets import QWidget
from unit.keyinput import key_data
from qutebrowser.keyinput import keyutils
-from qutebrowser.utils import utils
@pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute)
@@ -421,13 +420,10 @@ class TestKeySequence:
('', Qt.Key_Colon, Qt.AltModifier | Qt.ShiftModifier, ':',
'<Alt+Shift+:>'),
- # Swapping Control/Meta on macOS
- ('', Qt.Key_A, Qt.ControlModifier, '',
- '<Meta+A>' if utils.is_mac else '<Ctrl+A>'),
- ('', Qt.Key_A, Qt.ControlModifier | Qt.ShiftModifier, '',
- '<Meta+Shift+A>' if utils.is_mac else '<Ctrl+Shift+A>'),
- ('', Qt.Key_A, Qt.MetaModifier, '',
- '<Ctrl+A>' if utils.is_mac else '<Meta+A>'),
+ # Modifiers
+ ('', Qt.Key_A, Qt.ControlModifier, '', '<Ctrl+A>'),
+ ('', Qt.Key_A, Qt.ControlModifier | Qt.ShiftModifier, '', '<Ctrl+Shift+A>'),
+ ('', Qt.Key_A, Qt.MetaModifier, '', '<Meta+A>'),
# Handling of Backtab
('', Qt.Key_Backtab, Qt.NoModifier, '', '<Backtab>'),
@@ -444,27 +440,6 @@ class TestKeySequence:
new = seq.append_event(event)
assert new == keyutils.KeySequence.parse(expected)
- @pytest.mark.fake_os('mac')
- @pytest.mark.parametrize('modifiers, expected', [
- (Qt.ControlModifier,
- Qt.MetaModifier),
- (Qt.MetaModifier,
- Qt.ControlModifier),
- (Qt.ControlModifier | Qt.MetaModifier,
- Qt.ControlModifier | Qt.MetaModifier),
- (Qt.ControlModifier | Qt.ShiftModifier,
- Qt.MetaModifier | Qt.ShiftModifier),
- (Qt.MetaModifier | Qt.ShiftModifier,
- Qt.ControlModifier | Qt.ShiftModifier),
- (Qt.ShiftModifier, Qt.ShiftModifier),
- ])
- def test_fake_mac(self, modifiers, expected):
- """Make sure Control/Meta are swapped with a simulated Mac."""
- seq = keyutils.KeySequence()
- info = keyutils.KeyInfo(key=Qt.Key_A, modifiers=modifiers)
- new = seq.append_event(info.to_event())
- assert new[0] == keyutils.KeyInfo(Qt.Key_A, expected)
-
@pytest.mark.parametrize('key', [Qt.Key_unknown, 0x0])
def test_append_event_invalid(self, key):
seq = keyutils.KeySequence()
diff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py
index 8bf71aff4..bd37fd2dc 100644
--- a/tests/unit/mainwindow/statusbar/test_url.py
+++ b/tests/unit/mainwindow/statusbar/test_url.py
@@ -26,7 +26,6 @@ from PyQt5.QtCore import QUrl
from qutebrowser.utils import usertypes, urlutils
from qutebrowser.mainwindow.statusbar import url
-from helpers import utils
@pytest.fixture
@@ -47,10 +46,7 @@ def url_widget(qtbot, monkeypatch, config_stub):
('http://username:secret%20password@test.com', 'http://username@test.com'),
('http://example.com%5b/', '(invalid URL!) http://example.com%5b/'),
# https://bugreports.qt.io/browse/QTBUG-60364
- pytest.param('http://www.xn--80ak6aa92e.com',
- '(unparseable URL!) http://www.аррӏе.com', marks=utils.qt58),
- pytest.param('http://www.xn--80ak6aa92e.com',
- 'http://www.xn--80ak6aa92e.com', marks=utils.qt59),
+ ('http://www.xn--80ak6aa92e.com', 'http://www.xn--80ak6aa92e.com'),
# IDN URL
('http://www.ä.com', '(www.xn--4ca.com) http://www.ä.com'),
(None, ''),
diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py
index 659aac7ec..b271c18ab 100644
--- a/tests/unit/mainwindow/test_tabwidget.py
+++ b/tests/unit/mainwindow/test_tabwidget.py
@@ -94,8 +94,9 @@ class TestTabWidget:
config_stub.val.tabs.position = "left"
pinned_num = [1, num_tabs - 1]
- for tab in pinned_num:
- widget.set_tab_pinned(widget.widget(tab), True)
+ for num in pinned_num:
+ tab = widget.widget(num)
+ tab.set_pinned(True)
first_size = widget.tabBar().tabSizeHint(0)
first_size_min = widget.tabBar().minimumTabSizeHint(0)
diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py
index 5202efd07..2d4da12e8 100644
--- a/tests/unit/misc/test_checkpyver.py
+++ b/tests/unit/misc/test_checkpyver.py
@@ -28,21 +28,22 @@ import pytest
from qutebrowser.misc import checkpyver
-TEXT = (r"At least Python 3.5.2 is required to run qutebrowser, but it's "
+TEXT = (r"At least Python 3.6 is required to run qutebrowser, but it's "
r"running with \d+\.\d+\.\d+.")
@pytest.mark.not_frozen
-def test_python2():
- """Run checkpyver with python 2."""
+@pytest.mark.parametrize('python', ['python2', 'python3.5'])
+def test_old_python(python):
+ """Run checkpyver with old python versions."""
try:
proc = subprocess.run(
- ['python2', checkpyver.__file__, '--no-err-windows'],
+ [python, checkpyver.__file__, '--no-err-windows'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False)
except FileNotFoundError:
- pytest.skip("python2 not found")
+ pytest.skip(f"{python} not found")
assert not proc.stdout
stderr = proc.stderr.decode('utf-8').rstrip()
assert re.fullmatch(TEXT, stderr), stderr
diff --git a/tests/unit/misc/test_earlyinit.py b/tests/unit/misc/test_earlyinit.py
index 728b4eb26..af229a40a 100644
--- a/tests/unit/misc/test_earlyinit.py
+++ b/tests/unit/misc/test_earlyinit.py
@@ -31,3 +31,20 @@ def test_init_faulthandler_stderr_none(monkeypatch, attr):
"""Make sure init_faulthandler works when sys.stderr/__stderr__ is None."""
monkeypatch.setattr(sys, attr, None)
earlyinit.init_faulthandler()
+
+
+@pytest.mark.parametrize('same', [True, False])
+def test_qt_version(same):
+ if same:
+ qt_version_str = '5.14.0'
+ expected = '5.14.0'
+ else:
+ qt_version_str = '5.13.0'
+ expected = '5.14.0 (compiled 5.13.0)'
+ actual = earlyinit.qt_version(qversion='5.14.0', qt_version_str=qt_version_str)
+ assert actual == expected
+
+
+def test_qt_version_no_args():
+ """Make sure qt_version without arguments at least works."""
+ earlyinit.qt_version()
diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py
index 323ac1b21..694e6d204 100644
--- a/tests/unit/misc/test_editor.py
+++ b/tests/unit/misc/test_editor.py
@@ -137,17 +137,6 @@ class TestFileHandling:
msg = message_mock.getmsg(usertypes.MessageLevel.error)
assert msg.text.startswith("Failed to read back edited file: ")
- @pytest.fixture
- def unwritable_tmp_path(self, tmp_path):
- tmp_path.chmod(0)
- if os.access(str(tmp_path), os.W_OK):
- # Docker container or similar
- pytest.skip("File was still writable")
-
- yield tmp_path
-
- tmp_path.chmod(0o755)
-
def test_unwritable(self, monkeypatch, message_mock, editor,
unwritable_tmp_path, caplog):
"""Test file handling when the initial file is not writable."""
diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py
index dd4a5cc40..3f53ca238 100644
--- a/tests/unit/misc/test_ipc.py
+++ b/tests/unit/misc/test_ipc.py
@@ -767,7 +767,7 @@ def test_long_username(monkeypatch):
"""See https://github.com/qutebrowser/qutebrowser/issues/888."""
username = 'alexandercogneau'
basedir = '/this_is_a_long_basedir'
- monkeypatch.setattr('getpass.getuser', lambda: username)
+ monkeypatch.setattr(getpass, 'getuser', lambda: username)
name = ipc._get_socketname(basedir=basedir)
server = ipc.IPCServer(name)
expected_md5 = md5('{}-{}'.format(username, basedir))
diff --git a/tests/unit/misc/test_sql.py b/tests/unit/misc/test_sql.py
index 2cb55b891..c8efe6ef6 100644
--- a/tests/unit/misc/test_sql.py
+++ b/tests/unit/misc/test_sql.py
@@ -49,22 +49,8 @@ class TestSqlError:
with pytest.raises(exception):
sql.raise_sqlite_error("Message", sql_err)
- def test_qtbug_70506(self):
- """Test Qt's wrong handling of errors while opening the database.
-
- Due to https://bugreports.qt.io/browse/QTBUG-70506 we get an error with
- "out of memory" as string and -1 as error code.
- """
- sql_err = QSqlError("Error opening database",
- "out of memory",
- QSqlError.UnknownError,
- sql.SqliteErrorCode.UNKNOWN)
- with pytest.raises(sql.KnownError):
- sql.raise_sqlite_error("Message", sql_err)
-
def test_logging(self, caplog):
- sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError,
- '23')
+ sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError, '23')
with pytest.raises(sql.BugError):
sql.raise_sqlite_error("Message", sql_err)
diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py
index c398118cb..b64b8e0fe 100644
--- a/tests/unit/utils/test_jinja.py
+++ b/tests/unit/utils/test_jinja.py
@@ -115,7 +115,7 @@ def test_not_found(caplog):
def test_utf8():
- """Test rendering with an UTF8 template.
+ """Test rendering with a UTF8 template.
This was an attempt to get a failing test case for #127 but it seems
the issue is elsewhere.
diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py
index 3ae4c3cfc..8e8fa47a4 100644
--- a/tests/unit/utils/test_log.py
+++ b/tests/unit/utils/test_log.py
@@ -384,9 +384,9 @@ def test_stub(caplog, suffix, expected):
assert caplog.messages == [expected]
-def test_ignore_py_warnings(caplog):
+def test_py_warning_filter(caplog):
logging.captureWarnings(True)
- with log.ignore_py_warnings(category=UserWarning):
+ with log.py_warning_filter(category=UserWarning):
warnings.warn("hidden", UserWarning)
with caplog.at_level(logging.WARNING):
warnings.warn("not hidden", UserWarning)
@@ -395,6 +395,21 @@ def test_ignore_py_warnings(caplog):
assert msg.endswith("UserWarning: not hidden")
+def test_py_warning_filter_error(caplog):
+ warnings.simplefilter('ignore')
+ warnings.warn("hidden", UserWarning)
+
+ with log.py_warning_filter('error'):
+ with pytest.raises(UserWarning):
+ warnings.warn("error", UserWarning)
+
+
+def test_warning_still_errors():
+ # Mainly a sanity check after the tests messing with warnings above.
+ with pytest.raises(UserWarning):
+ warnings.warn("error", UserWarning)
+
+
class TestQtMessageHandler:
@attr.s
diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py
index 81d198946..2e54fb42e 100644
--- a/tests/unit/utils/test_qtutils.py
+++ b/tests/unit/utils/test_qtutils.py
@@ -26,6 +26,7 @@ import os.path
import unittest
import unittest.mock
+import attr
import pytest
from PyQt5.QtCore import (QDataStream, QPoint, QUrl, QByteArray, QIODevice,
QTimer, QBuffer, QFile, QProcess, QFileDevice)
@@ -53,23 +54,25 @@ else:
@pytest.mark.parametrize(['qversion', 'compiled', 'pyqt', 'version', 'exact',
'expected'], [
# equal versions
- ('5.4.0', None, None, '5.4.0', False, True),
- ('5.4.0', None, None, '5.4.0', True, True), # exact=True
- ('5.4.0', None, None, '5.4', True, True), # without trailing 0
+ ('5.14.0', None, None, '5.14.0', False, True),
+ ('5.14.0', None, None, '5.14.0', True, True), # exact=True
+ ('5.14.0', None, None, '5.14', True, True), # without trailing 0
# newer version installed
- ('5.4.1', None, None, '5.4', False, True),
- ('5.4.1', None, None, '5.4', True, False), # exact=True
+ ('5.14.1', None, None, '5.14', False, True),
+ ('5.14.1', None, None, '5.14', True, False), # exact=True
# older version installed
- ('5.3.2', None, None, '5.4', False, False),
- ('5.3.0', None, None, '5.3.2', False, False),
- ('5.3.0', None, None, '5.3.2', True, False), # exact=True
+ ('5.13.2', None, None, '5.14', False, False),
+ ('5.13.0', None, None, '5.13.2', False, False),
+ ('5.13.0', None, None, '5.13.2', True, False), # exact=True
# compiled=True
# new Qt runtime, but compiled against older version
- ('5.4.0', '5.3.0', '5.4.0', '5.4.0', False, False),
+ ('5.14.0', '5.13.0', '5.14.0', '5.14.0', False, False),
# new Qt runtime, compiled against new version, but old PyQt
- ('5.4.0', '5.4.0', '5.3.0', '5.4.0', False, False),
+ ('5.14.0', '5.14.0', '5.13.0', '5.14.0', False, False),
# all up-to-date
- ('5.4.0', '5.4.0', '5.4.0', '5.4.0', False, True),
+ ('5.14.0', '5.14.0', '5.14.0', '5.14.0', False, True),
+ # 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,
@@ -936,3 +939,107 @@ class TestEventLoop:
QTimer.singleShot(400, self.loop.quit)
self.loop.exec_()
assert not self.loop._executing
+
+
+class Color(QColor):
+
+ """A QColor with a nicer repr()."""
+
+ def __repr__(self):
+ return utils.get_repr(self, constructor=True, red=self.red(),
+ green=self.green(), blue=self.blue(),
+ alpha=self.alpha())
+
+
+class TestInterpolateColor:
+
+ @attr.s
+ class Colors:
+
+ white = attr.ib()
+ black = attr.ib()
+
+ @pytest.fixture
+ def colors(self):
+ """Example colors to be used."""
+ return self.Colors(Color('white'), Color('black'))
+
+ def test_invalid_start(self, colors):
+ """Test an invalid start color."""
+ with pytest.raises(qtutils.QtValueError):
+ qtutils.interpolate_color(Color(), colors.white, 0)
+
+ def test_invalid_end(self, colors):
+ """Test an invalid end color."""
+ with pytest.raises(qtutils.QtValueError):
+ qtutils.interpolate_color(colors.white, Color(), 0)
+
+ @pytest.mark.parametrize('perc', [-1, 101])
+ def test_invalid_percentage(self, colors, perc):
+ """Test an invalid percentage."""
+ with pytest.raises(ValueError):
+ qtutils.interpolate_color(colors.white, colors.white, perc)
+
+ def test_invalid_colorspace(self, colors):
+ """Test an invalid colorspace."""
+ with pytest.raises(ValueError):
+ qtutils.interpolate_color(colors.white, colors.black, 10, QColor.Cmyk)
+
+ @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv,
+ QColor.Hsl])
+ def test_0_100(self, colors, colorspace):
+ """Test 0% and 100% in different colorspaces."""
+ white = qtutils.interpolate_color(colors.white, colors.black, 0, colorspace)
+ black = qtutils.interpolate_color(colors.white, colors.black, 100, colorspace)
+ assert Color(white) == colors.white
+ assert Color(black) == colors.black
+
+ def test_interpolation_rgb(self):
+ """Test an interpolation in the RGB colorspace."""
+ color = qtutils.interpolate_color(
+ Color(0, 40, 100), Color(0, 20, 200), 50, QColor.Rgb)
+ assert Color(color) == Color(0, 30, 150)
+
+ def test_interpolation_hsv(self):
+ """Test an interpolation in the HSV colorspace."""
+ start = Color()
+ stop = Color()
+ start.setHsv(0, 40, 100)
+ stop.setHsv(0, 20, 200)
+ color = qtutils.interpolate_color(start, stop, 50, QColor.Hsv)
+ expected = Color()
+ expected.setHsv(0, 30, 150)
+ assert Color(color) == expected
+
+ def test_interpolation_hsl(self):
+ """Test an interpolation in the HSL colorspace."""
+ start = Color()
+ stop = Color()
+ start.setHsl(0, 40, 100)
+ stop.setHsl(0, 20, 200)
+ color = qtutils.interpolate_color(start, stop, 50, QColor.Hsl)
+ expected = Color()
+ expected.setHsl(0, 30, 150)
+ assert Color(color) == expected
+
+ @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv,
+ QColor.Hsl])
+ def test_interpolation_alpha(self, colorspace):
+ """Test interpolation of colorspace's alpha."""
+ start = Color(0, 0, 0, 30)
+ stop = Color(0, 0, 0, 100)
+ color = qtutils.interpolate_color(start, stop, 50, colorspace)
+ expected = Color(0, 0, 0, 65)
+ assert Color(color) == expected
+
+ @pytest.mark.parametrize('percentage, expected', [
+ (0, (0, 0, 0)),
+ (99, (0, 0, 0)),
+ (100, (255, 255, 255)),
+ ])
+ def test_interpolation_none(self, percentage, expected):
+ """Test an interpolation with a gradient turned off."""
+ color = qtutils.interpolate_color(
+ Color(0, 0, 0), Color(255, 255, 255), percentage, None)
+ assert isinstance(color, QColor)
+ assert Color(color) == Color(*expected)
diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py
index 064c51b30..dbc8831f6 100644
--- a/tests/unit/utils/test_standarddir.py
+++ b/tests/unit/utils/test_standarddir.py
@@ -28,7 +28,6 @@ import textwrap
import logging
import subprocess
-import attr
from PyQt5.QtCore import QStandardPaths
import pytest
@@ -108,7 +107,7 @@ def test_fake_windows(tmpdir, monkeypatch, what):
def test_fake_haiku(tmpdir, monkeypatch):
"""Test getting data dir on HaikuOS."""
locations = {
- QStandardPaths.DataLocation: '',
+ QStandardPaths.AppDataLocation: '',
QStandardPaths.ConfigLocation: str(tmpdir / 'config' / APPNAME),
}
monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation',
@@ -129,14 +128,14 @@ class TestWritableLocation:
'qutebrowser.utils.standarddir.QStandardPaths.writableLocation',
lambda typ: '')
with pytest.raises(standarddir.EmptyValueError):
- standarddir._writable_location(QStandardPaths.DataLocation)
+ standarddir._writable_location(QStandardPaths.AppDataLocation)
def test_sep(self, monkeypatch):
"""Make sure the right kind of separator is used."""
monkeypatch.setattr(standarddir.os, 'sep', '\\')
monkeypatch.setattr(standarddir.os.path, 'join',
lambda *parts: '\\'.join(parts))
- loc = standarddir._writable_location(QStandardPaths.DataLocation)
+ loc = standarddir._writable_location(QStandardPaths.AppDataLocation)
assert '/' not in loc
assert '\\' in loc
@@ -308,14 +307,12 @@ class TestInitCacheDirTag:
# http://www.brynosaurus.com/cachedir/
""").lstrip()
- def test_open_oserror(self, caplog, tmpdir, mocker, monkeypatch):
+ def test_open_oserror(self, caplog, unwritable_tmp_path, monkeypatch):
"""Test creating a new CACHEDIR.TAG."""
- monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir))
- mocker.patch('builtins.open', side_effect=OSError)
+ monkeypatch.setattr(standarddir, 'cache', lambda: str(unwritable_tmp_path))
with caplog.at_level(logging.ERROR, 'init'):
standarddir._init_cachedir_tag()
assert caplog.messages == ['Failed to create CACHEDIR.TAG']
- assert not tmpdir.listdir()
class TestCreatingDir:
@@ -394,133 +391,13 @@ class TestSystemData:
fake_args):
"""Test that system-wide path is not used on non-Linux OS."""
fake_args.basedir = str(tmpdir)
- monkeypatch.setattr('sys.platform', "potato")
+ monkeypatch.setattr(sys, 'platform', 'potato')
standarddir._init_data(args=fake_args)
assert standarddir.data(system=True) == standarddir.data()
-class TestMoveWindowsAndMacOS:
-
- """Test other invocations of _move_data."""
-
- @pytest.fixture(autouse=True)
- def patch_standardpaths(self, files, monkeypatch):
- locations = {
- QStandardPaths.DataLocation: str(files.local_data_dir),
- QStandardPaths.AppDataLocation: str(files.roaming_data_dir),
- }
- monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation',
- locations.get)
- monkeypatch.setattr(
- standarddir, 'config', lambda auto=False:
- str(files.auto_config_dir if auto else files.config_dir))
-
- @pytest.fixture
- def files(self, tmpdir):
-
- @attr.s
- class Files:
-
- auto_config_dir = attr.ib()
- config_dir = attr.ib()
- local_data_dir = attr.ib()
- roaming_data_dir = attr.ib()
-
- return Files(
- auto_config_dir=tmpdir / 'auto_config' / APPNAME,
- config_dir=tmpdir / 'config' / APPNAME,
- local_data_dir=tmpdir / 'data' / APPNAME,
- roaming_data_dir=tmpdir / 'roaming-data' / APPNAME,
- )
-
- def test_move_macos(self, files):
- """Test moving configs on macOS."""
- (files.auto_config_dir / 'autoconfig.yml').ensure()
- (files.auto_config_dir / 'quickmarks').ensure()
- files.config_dir.ensure(dir=True)
-
- standarddir._move_macos()
-
- assert (files.auto_config_dir / 'autoconfig.yml').exists()
- assert not (files.config_dir / 'autoconfig.yml').exists()
- assert not (files.auto_config_dir / 'quickmarks').exists()
- assert (files.config_dir / 'quickmarks').exists()
-
- def test_move_windows(self, files):
- """Test moving configs on Windows."""
- (files.local_data_dir / 'data' / 'blocked-hosts').ensure()
- (files.local_data_dir / 'qutebrowser.conf').ensure()
- (files.local_data_dir / 'cache' / 'cachefile').ensure()
-
- standarddir._move_windows()
-
- assert (files.roaming_data_dir / 'data' / 'blocked-hosts').exists()
- assert (files.roaming_data_dir / 'config' /
- 'qutebrowser.conf').exists()
- assert not (files.roaming_data_dir / 'cache').exists()
- assert (files.local_data_dir / 'cache' / 'cachefile').exists()
-
-
-class TestMove:
-
- @pytest.fixture
- def dirs(self, tmpdir):
- @attr.s
- class Dirs:
-
- old = attr.ib()
- new = attr.ib()
- old_file = attr.ib()
- new_file = attr.ib()
-
- old_dir = tmpdir / 'old'
- new_dir = tmpdir / 'new'
- return Dirs(old=old_dir, new=new_dir,
- old_file=old_dir / 'file', new_file=new_dir / 'file')
-
- def test_no_old_dir(self, dirs, caplog):
- """Nothing should happen without any old directory."""
- standarddir._move_data(str(dirs.old), str(dirs.new))
- assert not any(message.startswith('Migrating data from')
- for message in caplog.messages)
-
- @pytest.mark.parametrize('empty_dest', [True, False])
- def test_moving_data(self, dirs, empty_dest):
- dirs.old_file.ensure()
- if empty_dest:
- dirs.new.ensure(dir=True)
-
- standarddir._move_data(str(dirs.old), str(dirs.new))
- assert not dirs.old_file.exists()
- assert dirs.new_file.exists()
-
- def test_already_existing(self, dirs, caplog):
- dirs.old_file.ensure()
- dirs.new_file.ensure()
-
- with caplog.at_level(logging.ERROR):
- standarddir._move_data(str(dirs.old), str(dirs.new))
-
- expected = "Failed to move data from {} as {} is non-empty!".format(
- dirs.old, dirs.new)
- assert caplog.messages[-1] == expected
-
- def test_deleting_error(self, dirs, monkeypatch, mocker, caplog):
- """When there was an error it should be logged."""
- mock = mocker.Mock(side_effect=OSError('error'))
- monkeypatch.setattr(standarddir.shutil, 'move', mock)
- dirs.old_file.ensure()
-
- with caplog.at_level(logging.ERROR):
- standarddir._move_data(str(dirs.old), str(dirs.new))
-
- expected = "Failed to move data from {} to {}: error".format(
- dirs.old, dirs.new)
- assert caplog.messages[-1] == expected
-
-
@pytest.mark.parametrize('args_kind', ['basedir', 'normal', 'none'])
-def test_init(mocker, tmpdir, monkeypatch, args_kind):
+def test_init(tmpdir, monkeypatch, args_kind):
"""Do some sanity checks for standarddir.init().
Things like _init_cachedir_tag() are tested in more detail in other tests.
@@ -529,8 +406,6 @@ def test_init(mocker, tmpdir, monkeypatch, args_kind):
monkeypatch.setenv('HOME', str(tmpdir))
- m_windows = mocker.patch('qutebrowser.utils.standarddir._move_windows')
- m_mac = mocker.patch('qutebrowser.utils.standarddir._move_macos')
if args_kind == 'normal':
args = types.SimpleNamespace(basedir=None)
elif args_kind == 'basedir':
@@ -542,19 +417,6 @@ def test_init(mocker, tmpdir, monkeypatch, args_kind):
standarddir.init(args)
assert standarddir._locations != {}
- if args_kind == 'normal':
- if utils.is_mac:
- m_windows.assert_not_called()
- assert m_mac.called
- elif utils.is_windows:
- assert m_windows.called
- m_mac.assert_not_called()
- else:
- m_windows.assert_not_called()
- m_mac.assert_not_called()
- else:
- m_windows.assert_not_called()
- m_mac.assert_not_called()
@pytest.mark.linux
diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py
index 79504e9b2..c38794c40 100644
--- a/tests/unit/utils/test_urlmatch.py
+++ b/tests/unit/utils/test_urlmatch.py
@@ -28,7 +28,6 @@ Currently not tested:
- Any other features we don't need, such as .GetAsString() or set operations.
"""
-import sys
import string
import pytest
@@ -89,11 +88,7 @@ from qutebrowser.utils import urlmatch
("http://foo:/", "Invalid port: Port is empty"),
("http://*.foo:/", "Invalid port: Port is empty"),
("http://foo:com/", "Invalid port: .* 'com'"),
- pytest.param("http://foo:123456/",
- "Invalid port: Port out of range 0-65535",
- marks=pytest.mark.skipif(
- sys.hexversion < 0x03060000,
- reason="Doesn't show an error on Python 3.5")),
+ ("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"),
# No port specified, but port separator.
@@ -606,9 +601,9 @@ URL_TEXT = hst.text(alphabet=string.ascii_letters)
@hypothesis.given(pattern=hst.builds(
lambda *a: ''.join(a),
# Scheme
- hst.one_of(hst.just('*'), hst.just('http'), hst.just('file')),
+ hst.sampled_from(['*', 'http', 'file']),
# Separator
- hst.one_of(hst.just(':'), hst.just('://')),
+ hst.sampled_from([':', '://']),
# Host
hst.one_of(hst.just('*'),
hst.builds(lambda *a: ''.join(a), hst.just('*.'), URL_TEXT),
diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py
index 04f7f04e6..0167f6cee 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -33,7 +33,6 @@ import hypothesis.strategies
from qutebrowser.api import cmdutils
from qutebrowser.browser.network import pac
from qutebrowser.utils import utils, urlutils, usertypes
-from helpers import utils as testutils
class FakeDNS:
@@ -216,15 +215,11 @@ class TestFuzzyUrl:
assert url == QUrl('http://foo')
@pytest.mark.parametrize('do_search', [True, False])
- def test_invalid_url(self, do_search, is_url_mock, monkeypatch,
- caplog):
+ def test_invalid_url(self, do_search, caplog):
"""Test with an invalid URL."""
- is_url_mock.return_value = True
- monkeypatch.setattr(urlutils, 'qurl_from_user_input',
- lambda url: QUrl())
with pytest.raises(urlutils.InvalidUrlError):
with caplog.at_level(logging.ERROR):
- urlutils.fuzzy_url('foo', do_search=do_search)
+ urlutils.fuzzy_url('', do_search=do_search)
@pytest.mark.parametrize('url', ['', ' '])
def test_empty(self, url):
@@ -490,29 +485,6 @@ def test_searchengine_is_url(config_stub, auto_search, open_base_url, is_url):
assert urlutils.is_url('test') == is_url
-@pytest.mark.parametrize('user_input, output', [
- ('qutebrowser.org', 'http://qutebrowser.org'),
- ('http://qutebrowser.org', 'http://qutebrowser.org'),
- ('::1/foo', 'http://[::1]/foo'),
- ('[::1]/foo', 'http://[::1]/foo'),
- ('http://[::1]', 'http://[::1]'),
- ('qutebrowser.org', 'http://qutebrowser.org'),
- ('http://qutebrowser.org', 'http://qutebrowser.org'),
- ('::1/foo', 'http://[::1]/foo'),
- ('[::1]/foo', 'http://[::1]/foo'),
- ('http://[::1]', 'http://[::1]'),
-])
-def test_qurl_from_user_input(user_input, output):
- """Test qurl_from_user_input.
-
- Args:
- user_input: The string to pass to qurl_from_user_input.
- output: The expected QUrl string.
- """
- url = urlutils.qurl_from_user_input(user_input)
- assert url.toString() == output
-
-
@pytest.mark.parametrize('url, valid, has_err_string', [
('http://www.example.com/', True, False),
('', False, False),
@@ -643,7 +615,7 @@ class TestInvalidUrlError:
@pytest.mark.parametrize('are_same, url1, url2', [
(True, 'http://example.com', 'http://www.example.com'),
- (True, 'http://bbc.co.uk', 'https://www.bbc.co.uk'),
+ (True, 'http://bbc.co.uk', 'http://www.bbc.co.uk'),
(True, 'http://many.levels.of.domains.example.com', 'http://www.example.com'),
(True, 'http://idn.иком.museum', 'http://idn2.иком.museum'),
(True, 'http://one.not_a_valid_tld', 'http://one.not_a_valid_tld'),
@@ -652,6 +624,9 @@ class TestInvalidUrlError:
(False, 'https://example.kids.museum', 'http://example.kunst.museum'),
(False, 'http://idn.иком.museum', 'http://idn.ירושלים.museum'),
(False, 'http://one.not_a_valid_tld', 'http://two.not_a_valid_tld'),
+
+ (False, 'http://example.org', 'https://example.org'), # different scheme
+ (False, 'http://example.org:80', 'http://example.org:8080'), # different port
])
def test_same_domain(are_same, url1, url2):
"""Test same_domain."""
@@ -702,12 +677,8 @@ def test_data_url():
(QUrl('http://www.example.xn--p1ai'),
'(www.example.xn--p1ai) http://www.example.рф'),
# https://bugreports.qt.io/browse/QTBUG-60364
- pytest.param(QUrl('http://www.xn--80ak6aa92e.com'),
- '(unparseable URL!) http://www.аррӏе.com',
- marks=testutils.qt58),
- pytest.param(QUrl('http://www.xn--80ak6aa92e.com'),
- 'http://www.xn--80ak6aa92e.com',
- marks=testutils.qt59),
+ (QUrl('http://www.xn--80ak6aa92e.com'),
+ 'http://www.xn--80ak6aa92e.com'),
])
def test_safe_display_string(url, expected):
assert urlutils.safe_display_string(url) == expected
@@ -718,11 +689,6 @@ def test_safe_display_string_invalid():
urlutils.safe_display_string(QUrl())
-def test_query_string():
- url = QUrl('https://www.example.com/?foo=bar')
- assert urlutils.query_string(url) == 'foo=bar'
-
-
class TestProxyFromUrl:
@pytest.mark.parametrize('url, expected', [
@@ -745,9 +711,6 @@ class TestProxyFromUrl:
def test_proxy_from_url_valid(self, url, expected):
assert urlutils.proxy_from_url(QUrl(url)) == expected
- @pytest.mark.qt_log_ignore(
- r'^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not '
- r'de-queue request, failed to report HostNotFoundError')
@pytest.mark.parametrize('scheme', ['pac+http', 'pac+https'])
def test_proxy_from_url_pac(self, scheme, qapp):
fetcher = urlutils.proxy_from_url(QUrl('{}://foo'.format(scheme)))
@@ -781,6 +744,8 @@ class TestParseJavascriptUrl:
@pytest.mark.parametrize('url, source', [
(QUrl('javascript:"hello" %0a "world"'), '"hello" \n "world"'),
+ (QUrl('javascript:/'), '/'),
+ (QUrl('javascript:///'), '///'),
# https://github.com/web-platform-tests/wpt/blob/master/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-query-fragment-components.html
(QUrl('javascript:"nope" ? "yep" : "what";'), '"nope" ? "yep" : "what";'),
(QUrl('javascript:"wrong"; // # %0a "ok";'), '"wrong"; // # \n "ok";'),
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
index 3eda4234f..ac7ed5ce7 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -25,37 +25,25 @@ import os.path
import io
import logging
import functools
-import socket
import re
import shlex
import math
-import pkg_resources
-import attr
from PyQt5.QtCore import QUrl
-from PyQt5.QtGui import QColor, QClipboard
+from PyQt5.QtGui import QClipboard
import pytest
import hypothesis
from hypothesis import strategies
+import yaml
import qutebrowser
import qutebrowser.utils # for test_qualname
-from qutebrowser.utils import utils, qtutils, version, usertypes
+from qutebrowser.utils import utils, version, usertypes
ELLIPSIS = '\u2026'
-class Color(QColor):
-
- """A QColor with a nicer repr()."""
-
- def __repr__(self):
- return utils.get_repr(self, constructor=True, red=self.red(),
- green=self.green(), blue=self.blue(),
- alpha=self.alpha())
-
-
class TestCompactText:
"""Test compact_text."""
@@ -160,100 +148,6 @@ def test_resource_filename():
assert f.read().splitlines()[0] == "Hello World!"
-class TestInterpolateColor:
-
- """Tests for interpolate_color.
-
- Attributes:
- white: The Color white as a valid Color for tests.
- white: The Color black as a valid Color for tests.
- """
-
- @attr.s
- class Colors:
-
- white = attr.ib()
- black = attr.ib()
-
- @pytest.fixture
- def colors(self):
- """Example colors to be used."""
- return self.Colors(Color('white'), Color('black'))
-
- def test_invalid_start(self, colors):
- """Test an invalid start color."""
- with pytest.raises(qtutils.QtValueError):
- utils.interpolate_color(Color(), colors.white, 0)
-
- def test_invalid_end(self, colors):
- """Test an invalid end color."""
- with pytest.raises(qtutils.QtValueError):
- utils.interpolate_color(colors.white, Color(), 0)
-
- @pytest.mark.parametrize('perc', [-1, 101])
- def test_invalid_percentage(self, colors, perc):
- """Test an invalid percentage."""
- with pytest.raises(ValueError):
- utils.interpolate_color(colors.white, colors.white, perc)
-
- def test_invalid_colorspace(self, colors):
- """Test an invalid colorspace."""
- with pytest.raises(ValueError):
- utils.interpolate_color(colors.white, colors.black, 10,
- QColor.Cmyk)
-
- @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv,
- QColor.Hsl])
- def test_0_100(self, colors, colorspace):
- """Test 0% and 100% in different colorspaces."""
- white = utils.interpolate_color(colors.white, colors.black, 0,
- colorspace)
- black = utils.interpolate_color(colors.white, colors.black, 100,
- colorspace)
- assert Color(white) == colors.white
- assert Color(black) == colors.black
-
- def test_interpolation_rgb(self):
- """Test an interpolation in the RGB colorspace."""
- color = utils.interpolate_color(Color(0, 40, 100), Color(0, 20, 200),
- 50, QColor.Rgb)
- assert Color(color) == Color(0, 30, 150)
-
- def test_interpolation_hsv(self):
- """Test an interpolation in the HSV colorspace."""
- start = Color()
- stop = Color()
- start.setHsv(0, 40, 100)
- stop.setHsv(0, 20, 200)
- color = utils.interpolate_color(start, stop, 50, QColor.Hsv)
- expected = Color()
- expected.setHsv(0, 30, 150)
- assert Color(color) == expected
-
- def test_interpolation_hsl(self):
- """Test an interpolation in the HSL colorspace."""
- start = Color()
- stop = Color()
- start.setHsl(0, 40, 100)
- stop.setHsl(0, 20, 200)
- color = utils.interpolate_color(start, stop, 50, QColor.Hsl)
- expected = Color()
- expected.setHsl(0, 30, 150)
- assert Color(color) == expected
-
- @pytest.mark.parametrize('percentage, expected', [
- (0, (0, 0, 0)),
- (99, (0, 0, 0)),
- (100, (255, 255, 255)),
- ])
- def test_interpolation_none(self, percentage, expected):
- """Test an interpolation with a gradient turned off."""
- color = utils.interpolate_color(Color(0, 0, 0), Color(255, 255, 255),
- percentage, None)
- assert isinstance(color, QColor)
- assert Color(color) == Color(*expected)
-
-
@pytest.mark.parametrize('seconds, out', [
(-1, '-0:01'),
(0, '0:00'),
@@ -560,8 +454,12 @@ class TestIsEnum:
def test_enum(self):
"""Test is_enum with an enum."""
- e = enum.Enum('Foo', 'bar, baz')
- assert utils.is_enum(e)
+ class Foo(enum.Enum):
+
+ bar = enum.auto()
+ baz = enum.auto()
+
+ assert utils.is_enum(Foo)
def test_class(self):
"""Test is_enum with a non-enum class."""
@@ -745,13 +643,6 @@ class TestGetSetClipboard:
utils.get_clipboard(fallback=True)
-def test_random_port():
- port = utils.random_port()
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.bind(('localhost', port))
- sock.close()
-
-
class TestOpenFile:
@pytest.mark.not_frozen
@@ -800,7 +691,7 @@ class TestOpenFile:
info = version.DistributionInfo(
id='org.kde.Platform',
parsed=version.Distribution.kde_flatpak,
- version=pkg_resources.parse_version('5.12'),
+ version=utils.parse_version('5.12'),
pretty='Unknown')
monkeypatch.setattr(version, 'distribution',
lambda: info)
@@ -852,6 +743,10 @@ class TestYaml:
def test_load(self):
assert utils.yaml_load("[1, 2]") == [1, 2]
+ def test_load_float_bug(self):
+ with pytest.raises(yaml.YAMLError):
+ utils.yaml_load("._")
+
def test_load_file(self, tmpdir):
tmpfile = tmpdir / 'foo.yml'
tmpfile.write('[1, 2]')
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index bc369ba15..593557ae8 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -25,7 +25,7 @@ import collections
import os.path
import subprocess
import contextlib
-import builtins # noqa https://github.com/JBKahn/flake8-debugger/issues/20
+import builtins
import types
import importlib
import logging
@@ -33,7 +33,6 @@ import textwrap
import datetime
import attr
-import pkg_resources
import pytest
import hypothesis
import hypothesis.strategies
@@ -41,7 +40,7 @@ import hypothesis.strategies
import qutebrowser
from qutebrowser.config import config
from qutebrowser.utils import version, usertypes, utils, standarddir
-from qutebrowser.misc import pastebin
+from qutebrowser.misc import pastebin, objects
from qutebrowser.browser import pdfjs
@@ -77,7 +76,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='ubuntu', parsed=version.Distribution.ubuntu,
- version=pkg_resources.parse_version('14.4'),
+ version=utils.parse_version('14.4'),
pretty='Ubuntu 14.04.5 LTS')),
# Ubuntu 17.04
("""
@@ -90,7 +89,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='ubuntu', parsed=version.Distribution.ubuntu,
- version=pkg_resources.parse_version('17.4'),
+ version=utils.parse_version('17.4'),
pretty='Ubuntu 17.04')),
# Debian Jessie
("""
@@ -102,7 +101,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='debian', parsed=version.Distribution.debian,
- version=pkg_resources.parse_version('8'),
+ version=utils.parse_version('8'),
pretty='Debian GNU/Linux 8 (jessie)')),
# Void Linux
("""
@@ -133,7 +132,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='fedora', parsed=version.Distribution.fedora,
- version=pkg_resources.parse_version('25'),
+ version=utils.parse_version('25'),
pretty='Fedora 25 (Twenty Five)')),
# OpenSUSE
("""
@@ -146,7 +145,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='opensuse', parsed=version.Distribution.opensuse,
- version=pkg_resources.parse_version('42.2'),
+ version=utils.parse_version('42.2'),
pretty='openSUSE Leap 42.2')),
# Linux Mint
("""
@@ -159,7 +158,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='linuxmint', parsed=version.Distribution.linuxmint,
- version=pkg_resources.parse_version('18.1'),
+ version=utils.parse_version('18.1'),
pretty='Linux Mint 18.1')),
# Manjaro
("""
@@ -188,7 +187,7 @@ from qutebrowser.browser import pdfjs
""",
version.DistributionInfo(
id='org.kde.Platform', parsed=version.Distribution.kde_flatpak,
- version=pkg_resources.parse_version('5.12'),
+ version=utils.parse_version('5.12'),
pretty='KDE')),
# No PRETTY_NAME
("""
@@ -221,7 +220,7 @@ def test_distribution(tmpdir, monkeypatch, os_release, expected):
(None, False),
(version.DistributionInfo(
id='org.kde.Platform', parsed=version.Distribution.kde_flatpak,
- version=pkg_resources.parse_version('5.12'),
+ version=utils.parse_version('5.12'),
pretty='Unknown'), True),
(version.DistributionInfo(
id='arch', parsed=version.Distribution.arch, version=None,
@@ -561,7 +560,6 @@ class ImportFake:
('jinja2', True),
('pygments', True),
('yaml', True),
- ('cssutils', True),
('attr', True),
('PyQt5.QtWebEngineWidgets', True),
('PyQt5.QtWebEngine', True),
@@ -615,7 +613,7 @@ class ImportFake:
def import_fake(monkeypatch):
"""Fixture to patch imports using ImportFake."""
fake = ImportFake()
- monkeypatch.setattr('builtins.__import__', fake.fake_import)
+ monkeypatch.setattr(builtins, '__import__', fake.fake_import)
monkeypatch.setattr(version.importlib, 'import_module',
fake.fake_importlib_import)
return fake
@@ -637,7 +635,6 @@ class TestModuleVersions:
@pytest.mark.parametrize('module, idx, expected', [
('colorama', 1, 'colorama: no'),
- ('cssutils', 6, 'cssutils: no'),
])
def test_missing_module(self, module, idx, expected, import_fake):
"""Test with a module missing.
@@ -681,7 +678,6 @@ class TestModuleVersions:
('jinja2', True),
('pygments', True),
('yaml', True),
- ('cssutils', True),
('attr', True),
])
def test_existing_attributes(self, name, has_version):
@@ -694,8 +690,6 @@ class TestModuleVersions:
name: The name of the module to check.
has_version: Whether a __version__ attribute is expected.
"""
- if name == 'cssutils':
- pytest.importorskip(name)
module = importlib.import_module(name)
assert hasattr(module, '__version__') == has_version
@@ -868,39 +862,49 @@ _QTWE_USER_AGENT = ("Mozilla/5.0 (X11; Linux x86_64) "
"QtWebEngine/5.14.0 Chrome/{} Safari/537.36")
-def test_chromium_version(monkeypatch, caplog):
- pytest.importorskip('PyQt5.QtWebEngineWidgets')
+class TestChromiumVersion:
- ver = '77.0.3865.98'
- version.webenginesettings._init_user_agent_str(
- _QTWE_USER_AGENT.format(ver))
+ @pytest.fixture(autouse=True)
+ def clear_parsed_ua(self, monkeypatch):
+ if version.webenginesettings is not None:
+ # Not available with QtWebKit
+ monkeypatch.setattr(version.webenginesettings, 'parsed_user_agent', None)
- assert version._chromium_version() == ver
+ def test_fake_ua(self, monkeypatch, caplog):
+ pytest.importorskip('PyQt5.QtWebEngineWidgets')
+ ver = '77.0.3865.98'
+ version.webenginesettings._init_user_agent_str(
+ _QTWE_USER_AGENT.format(ver))
-def test_chromium_version_no_webengine(monkeypatch):
- monkeypatch.setattr(version, 'webenginesettings', None)
- assert version._chromium_version() == 'unavailable'
+ assert version._chromium_version() == ver
+ def test_no_webengine(self, monkeypatch):
+ monkeypatch.setattr(version, 'webenginesettings', None)
+ assert version._chromium_version() == 'unavailable'
-def test_chromium_version_prefers_saved_user_agent(monkeypatch):
- pytest.importorskip('PyQt5.QtWebEngineWidgets')
- version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT)
+ def test_prefers_saved_user_agent(self, monkeypatch):
+ pytest.importorskip('PyQt5.QtWebEngineWidgets')
+ version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT)
- class FakeProfile:
- def defaultProfile(self):
- raise AssertionError("Should not be called")
+ class FakeProfile:
+ def defaultProfile(self):
+ raise AssertionError("Should not be called")
- monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile',
- FakeProfile())
+ monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile',
+ FakeProfile())
- version._chromium_version()
+ version._chromium_version()
+ def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub):
+ pytest.importorskip('PyQt5.QtWebEngineWidgets')
+ unexpected = ['', 'unknown', 'unavailable', 'avoided']
+ assert version._chromium_version() not in unexpected
-def test_chromium_version_unpatched(qapp, cache_tmpdir, data_tmpdir,
- config_stub):
- pytest.importorskip('PyQt5.QtWebEngineWidgets')
- assert version._chromium_version() not in ['', 'unknown', 'unavailable']
+ def test_avoided(self, monkeypatch):
+ pytest.importorskip('PyQt5.QtWebEngineWidgets')
+ monkeypatch.setattr(objects, 'debug_flags', ['avoid-chromium-init'])
+ assert version._chromium_version() == 'avoided'
@attr.s
@@ -1013,7 +1017,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub):
substitutions['ssl'] = 'SSL VERSION' if params.ssl_support else 'no'
for name, val in patches.items():
- monkeypatch.setattr('qutebrowser.utils.version.' + name, val)
+ monkeypatch.setattr(f'qutebrowser.utils.version.{name}', val)
if params.frozen:
monkeypatch.setattr(sys, 'frozen', True, raising=False)
@@ -1128,9 +1132,8 @@ def pbclient(stubs):
def test_pastebin_version(pbclient, message_mock, monkeypatch, qtbot):
"""Test version.pastebin_version() sets the url."""
- monkeypatch.setattr('qutebrowser.utils.version.version_info',
- lambda: "dummy")
- monkeypatch.setattr('qutebrowser.utils.utils.log_clipboard', True)
+ monkeypatch.setattr(version, 'version_info', lambda: 'dummy')
+ monkeypatch.setattr(utils, 'log_clipboard', True)
version.pastebin_version(pbclient)
pbclient.success.emit("https://www.example.com/\n")
@@ -1143,8 +1146,7 @@ def test_pastebin_version(pbclient, message_mock, monkeypatch, qtbot):
def test_pastebin_version_twice(pbclient, monkeypatch):
"""Test whether calling pastebin_version twice sends no data."""
- monkeypatch.setattr('qutebrowser.utils.version.version_info',
- lambda: "dummy")
+ monkeypatch.setattr(version, 'version_info', lambda: 'dummy')
version.pastebin_version(pbclient)
pbclient.success.emit("https://www.example.com/\n")
@@ -1161,8 +1163,7 @@ def test_pastebin_version_twice(pbclient, monkeypatch):
def test_pastebin_version_error(pbclient, caplog, message_mock, monkeypatch):
"""Test version.pastebin_version() with errors."""
- monkeypatch.setattr('qutebrowser.utils.version.version_info',
- lambda: "dummy")
+ monkeypatch.setattr(version, 'version_info', lambda: 'dummy')
version.pastebin_url = None
with caplog.at_level(logging.ERROR):
@@ -1182,7 +1183,7 @@ def test_uptime(monkeypatch, qapp):
class FakeDateTime(datetime.datetime):
now = lambda x=datetime.datetime(1, 1, 1, 1, 1, 1, 2): x
- monkeypatch.setattr('datetime.datetime', FakeDateTime)
+ monkeypatch.setattr(datetime, 'datetime', FakeDateTime)
uptime_delta = version._uptime()
assert uptime_delta == datetime.timedelta(0)
diff --git a/tox.ini b/tox.ini
index 369d4afe6..abdce0b5b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,30 +12,26 @@ minversion = 3.15
[testenv]
setenv =
PYTEST_QT_API=pyqt5
- pyqt{,57,59,510,511,512,513,514,515}: LINK_PYQT_SKIP=true
- pyqt{,57,59,510,511,512,513,514,515}: QUTE_BDD_WEBENGINE=true
+ 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
basepython =
+ py: {env:PYTHON:python3}
py3: {env:PYTHON:python3}
- py35: {env:PYTHON:python3.5}
py36: {env:PYTHON:python3.6}
py37: {env:PYTHON:python3.7}
py38: {env:PYTHON:python3.8}
py39: {env:PYTHON:python3.9}
-pip_version = pip
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-tests.txt
pyqt: -r{toxinidir}/misc/requirements/requirements-pyqt.txt
- pyqt57: -r{toxinidir}/misc/requirements/requirements-pyqt-5.7.txt
- pyqt59: -r{toxinidir}/misc/requirements/requirements-pyqt-5.9.txt
- pyqt510: -r{toxinidir}/misc/requirements/requirements-pyqt-5.10.txt
- pyqt511: -r{toxinidir}/misc/requirements/requirements-pyqt-5.11.txt
pyqt512: -r{toxinidir}/misc/requirements/requirements-pyqt-5.12.txt
pyqt513: -r{toxinidir}/misc/requirements/requirements-pyqt-5.13.txt
pyqt514: -r{toxinidir}/misc/requirements/requirements-pyqt-5.14.txt
pyqt515: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.txt
+ pyqt5150: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.0.txt
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -bb -m pytest {posargs:tests}
@@ -46,19 +42,14 @@ commands =
[testenv:misc]
ignore_errors = true
basepython = {env:PYTHON:python3}
-pip_version = pip
# For global .gitignore files
passenv = HOME
deps =
commands =
- {envpython} scripts/dev/misc_checks.py git
- {envpython} scripts/dev/misc_checks.py vcs
- {envpython} scripts/dev/misc_checks.py spelling
- {envpython} scripts/dev/misc_checks.py userscripts
+ {envpython} scripts/dev/misc_checks.py {posargs:all}
[testenv:vulture]
basepython = {env:PYTHON:python3}
-pip_version = pip
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-vulture.txt
@@ -69,7 +60,6 @@ commands =
[testenv:vulture-pyqtlink]
basepython = {env:PYTHON:python3}
-pip_version = pip
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-vulture.txt
@@ -80,7 +70,6 @@ commands =
[testenv:pylint]
basepython = {env:PYTHON:python3}
-pip_version = pip
ignore_errors = true
passenv =
deps =
@@ -94,7 +83,6 @@ commands =
[testenv:pylint-pyqtlink]
basepython = {env:PYTHON:python3}
-pip_version = pip
ignore_errors = true
passenv =
deps =
@@ -108,7 +96,6 @@ commands =
[testenv:pylint-master]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = {[testenv:pylint]passenv}
deps =
-r{toxinidir}/requirements.txt
@@ -121,7 +108,6 @@ commands =
[testenv:flake8]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv =
deps =
-r{toxinidir}/requirements.txt
@@ -131,7 +117,6 @@ commands =
[testenv:pyroma]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv =
deps =
-r{toxinidir}/misc/requirements/requirements-pyroma.txt
@@ -140,7 +125,6 @@ commands =
[testenv:check-manifest]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv =
deps =
-r{toxinidir}/misc/requirements/requirements-check-manifest.txt
@@ -149,7 +133,6 @@ commands =
[testenv:docs]
basepython = {env:PYTHON:python3}
-pip_version = pip
whitelist_externals = git
passenv = CI GITHUB_REF
deps =
@@ -162,7 +145,6 @@ commands =
[testenv:pyinstaller-{64,32}]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = APPDATA HOME PYINSTALLER_DEBUG
deps =
-r{toxinidir}/requirements.txt
@@ -187,7 +169,6 @@ commands = bash scripts/dev/run_shellcheck.sh {posargs}
[testenv:mypy]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = TERM MYPY_FORCE_TERMINAL_WIDTH
deps =
-r{toxinidir}/requirements.txt
@@ -199,14 +180,12 @@ commands =
[testenv:yamllint]
basepython = {env:PYTHON:python3}
-pip_version = pip
deps = -r{toxinidir}/misc/requirements/requirements-yamllint.txt
commands =
{envpython} -m yamllint -f colored --strict . {posargs}
[testenv:mypy-diff]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv = {[testenv:mypy]passenv}
deps = {[testenv:mypy]deps}
commands =
@@ -215,7 +194,6 @@ commands =
[testenv:sphinx]
basepython = {env:PYTHON:python3}
-pip_version = pip
passenv =
usedevelop = true
deps =
@@ -224,3 +202,16 @@ deps =
-r{toxinidir}/misc/requirements/requirements-sphinx.txt
commands =
{envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/
+
+[testenv:build-release]
+basepython = {env:PYTHON:python3}
+passenv = *
+usedevelop = true
+deps =
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/misc/requirements/requirements-tox.txt
+ -r{toxinidir}/misc/requirements/requirements-pyqt.txt
+ -r{toxinidir}/misc/requirements/requirements-dev.txt
+ -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt
+commands =
+ {envpython} {toxinidir}/scripts/dev/build_release.py {posargs}